Skip to content

Commit

Permalink
Implement folder caching in PermanentFilesystem
Browse files Browse the repository at this point in the history
When calling the "navigate" method from PermanentFilesystem, save the
result in a cache and try to fetch from cache when looking up the same
folder later.

This cache is not meant to actually reduce the number of network calls,
but rather to provide a somewhat accurate temporary cached value while
the browser is waiting for the API to respond back with the true value.

As a result, the code is designed in such a way that any objects
returned from the `getFolder` method may automatically after the
function is called.

PER-9463: Add caching to folder navigate calls
  • Loading branch information
meisekimiu committed Mar 6, 2024
1 parent e8c0b7a commit 7bd51e3
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 4 deletions.
69 changes: 69 additions & 0 deletions src/app/filesystem/folder-cache.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { FolderVO } from '@models/index';
import { FolderCache } from './folder-cache';

describe('FolderCache', () => {
let cache: FolderCache;
beforeEach(() => {
cache = new FolderCache();
});
it('should exist', () => {
expect(cache).toBeTruthy();
});
it('should return null if a folder is not cached', () => {
cache.saveFolder(new FolderVO({ folder_linkId: 10000 }));

expect(cache.getFolder({ folder_linkId: 1 })).toBeNull();
});
it('should be able to save to the cache', () => {
const folder = new FolderVO({ folder_linkId: 1 });
cache.saveFolder(folder);

expect(cache.getFolder({ folder_linkId: 1 })).toEqual(folder);
});
it('should be able to update an existing cache value', () => {
const folder = new FolderVO({ folder_linkId: 1 });
cache.saveFolder(folder);
cache.saveFolder(
new FolderVO({ folder_linkId: 1, displayName: 'Unit Test' })
);

expect(cache.getFolder({ folder_linkId: 1 }).displayName).toBe('Unit Test');
});

describe('Folder Identifiers', () => {
let folder: FolderVO;

beforeEach(() => {
folder = new FolderVO({
folderId: 1,
folder_linkId: 10,
archiveNbr: '1234-test',
});
cache.saveFolder(folder);
});

it('can look up by FolderId', () => {
expect(cache.getFolder({ folderId: 1 })).toEqual(folder);
});

it('can look up by folder_linkId', () => {
expect(cache.getFolder({ folder_linkId: 10 })).toEqual(folder);
});

it('can look up by ArchiveNbr', () => {
expect(cache.getFolder({ archiveNbr: '1234-test' })).toEqual(folder);
});

it('should prioritize folderId over folder_linkId', () => {
expect(cache.getFolder({ folderId: 1, folder_linkId: Infinity })).toEqual(
folder
);
});

it('should prioritize folder_linkId over archiveNbr', () => {
expect(
cache.getFolder({ folder_linkId: 10, archiveNbr: 'No Match' })
).toEqual(folder);
});
});
});
34 changes: 34 additions & 0 deletions src/app/filesystem/folder-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { FolderVO } from '@models/index';
import { FolderIdentifier } from './types/filesystem-identifier';
import { KeysOfUnion } from './types/keysofunion';

export class FolderCache {
private folders: FolderVO[] = [];

private fetchFromCache(
query: FolderIdentifier,
property: KeysOfUnion<FolderIdentifier>
): FolderVO | undefined {
if (property in query) {
return this.folders.find((f) => f[property] === query[property]);
}
}

public getFolder(folder: FolderIdentifier): FolderVO | null {
return (
this.fetchFromCache(folder, 'folderId') ||
this.fetchFromCache(folder, 'folder_linkId') ||
this.fetchFromCache(folder, 'archiveNbr') ||
null
);
}

public saveFolder(folder: FolderVO): void {
const cachedFolder = this.getFolder(folder);
if (cachedFolder) {
Object.assign(cachedFolder, folder);
} else {
this.folders.push(folder);
}
}
}
15 changes: 14 additions & 1 deletion src/app/filesystem/mocks/fake-filesystem-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,19 @@ import { ArchiveIdentifier } from '../types/archive-identifier';

export class FakeFilesystemApi implements FilesystemApi {
private calledMethods: string[] = [];
private folders: FolderVO[] = [];

public async navigate(folder: FolderIdentifier) {
this.logCall('navigate');
return new FolderVO({});
if ('folderId' in folder) {
const fetchedFolder = this.folders.find(
(f) => f.folderId === folder.folderId
);
if (fetchedFolder) {
return fetchedFolder;
}
}
return new FolderVO(folder);
}

public async getRoot(archive: ArchiveIdentifier) {
Expand All @@ -28,6 +37,10 @@ export class FakeFilesystemApi implements FilesystemApi {
return this.calledMethods.includes(name);
}

public addFolder(folder: FolderVO): void {
this.folders.push(folder);
}

private logCall(name: string) {
this.calledMethods.push(name);
}
Expand Down
43 changes: 42 additions & 1 deletion src/app/filesystem/permanent-filesystem.spec.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
import { FolderVO } from '@models/index';
import { PermanentFilesystem } from './permanent-filesystem';
import { FakeFilesystemApi } from './mocks/fake-filesystem-api';
import { FolderCache } from './folder-cache';

describe('Permanent Filesystem (Folder Caching)', () => {
let fs: PermanentFilesystem;
let api: FakeFilesystemApi;
let cache: FolderCache;

beforeEach(() => {
api = new FakeFilesystemApi();
fs = new PermanentFilesystem(api);
cache = new FolderCache();
fs = new PermanentFilesystem(api, cache);
});

it('should exist', () => {
expect(fs).toBeTruthy();
});

it('does not need to have an external FolderCache provided', () => {
fs = new PermanentFilesystem(api);

expect(fs).toBeTruthy();
});

it('should be able to fetch the filesystem root', async () => {
const root = await fs.getArchiveRoot({ archiveId: 0 });

Expand All @@ -34,4 +44,35 @@ describe('Permanent Filesystem (Folder Caching)', () => {
expect(record).toBeTruthy();
expect(api.methodWasCalled('recordGet')).toBeTrue();
});

it('should save fetched values to the cache', async () => {
await fs.getFolder({ folderId: 0 });

expect(cache.getFolder({ folderId: 0 })).not.toBeNull();
});

it('should use the cached version of a folder immediately', async () => {
api.addFolder(new FolderVO({ folderId: 0, displayName: 'Updated Value' }));
cache.saveFolder(
new FolderVO({ folderId: 0, displayName: 'Cached Value' })
);
// Copy the initial value returned from the cache so it will never be modified
const folder = Object.assign({}, await fs.getFolder({ folderId: 0 }));

expect(folder.displayName).toBe('Cached Value');
});

it('should still fetch and update a folder after retrieving the cached version', async (done) => {
cache.saveFolder(
new FolderVO({ folderId: 0, displayName: 'Cached Value' })
);
// Simulate a backend change happening in another window/tab/client/etc.
api.addFolder(new FolderVO({ folderId: 0, displayName: 'Updated Value' }));
const folder = await fs.getFolder({ folderId: 0 });

setTimeout(() => {
expect(folder.displayName).toBe('Updated Value');
done();
}, 0);
});
});
26 changes: 24 additions & 2 deletions src/app/filesystem/permanent-filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,38 @@ import {
FolderIdentifier,
RecordIdentifier,
} from './types/filesystem-identifier';
import { FolderCache } from './folder-cache';

export class PermanentFilesystem {
constructor(private api: FilesystemApi) {}
private folderCache: FolderCache;

constructor(private api: FilesystemApi, private cache?: FolderCache) {
if (cache) {
this.folderCache = cache;
} else {
this.folderCache = new FolderCache();
}
}

public async getArchiveRoot(archive: ArchiveIdentifier): Promise<FolderVO> {
return await this.api.getRoot(archive);
}

public async getFolder(folder: FolderIdentifier): Promise<FolderVO> {
return await this.api.navigate(folder);
const cachedFolder = this.folderCache.getFolder(folder);
if (cachedFolder) {
setTimeout(() => {
this.fetchFolderFromApi(folder);
}, 0);
return cachedFolder;
}
return await this.fetchFolderFromApi(folder);
}

private async fetchFolderFromApi(folder: FolderIdentifier) {
const fetchedFolder = await this.api.navigate(folder);
this.folderCache.saveFolder(fetchedFolder);
return fetchedFolder;
}

public async getRecord(record: RecordIdentifier): Promise<RecordVO> {
Expand Down
3 changes: 3 additions & 0 deletions src/app/filesystem/types/keysofunion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Sourced from this Stack Overflow answer by user Titian Cernicova-Dragomir:
// https://stackoverflow.com/a/49402091
export type KeysOfUnion<T> = T extends T ? keyof T : never;

0 comments on commit 7bd51e3

Please sign in to comment.