Skip to content

Commit

Permalink
feat(format): add FormatManager
Browse files Browse the repository at this point in the history
  • Loading branch information
fallenoak committed Dec 27, 2023
1 parent 63ddd52 commit 7d1873e
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 0 deletions.
65 changes: 65 additions & 0 deletions src/lib/FormatManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import AssetManager from './AssetManager';

interface FormatConstructor<T> {
new (...args: any[]): T;
}

interface Format<T> {
load: (data: ArrayBuffer) => T;
}

class FormatManager {
#assetManager: AssetManager;
#loaded = new Map<string, any>();
#loading = new Map<string, Promise<any>>();

constructor(assetManager: AssetManager) {
this.#assetManager = assetManager;
}

get<T extends Format<T>>(
path: string,
FormatClass: FormatConstructor<T>,
...formatConstructorArgs: any[]
): Promise<T> {
const cacheKey = `${path}:${FormatClass.prototype.constructor.name}:${formatConstructorArgs}`;

const loaded = this.#loaded.get(cacheKey);
if (loaded) {
return Promise.resolve(loaded);
}

const alreadyLoading = this.#loading.get(cacheKey);
if (alreadyLoading) {
return alreadyLoading;
}

const loading = this.#load(cacheKey, path, FormatClass, formatConstructorArgs);
this.#loading.set(cacheKey, loading);

return loading;
}

async #load<T extends Format<T>>(
cacheKey: string,
path: string,
FormatClass: FormatConstructor<T>,
formatConstructorArgs: any[],
): Promise<T> {
let instance: T;
try {
const data = await this.#assetManager.get(path);
instance = new FormatClass(...formatConstructorArgs).load(data);

this.#loaded.set(cacheKey, instance);
} finally {
this.#loading.delete(cacheKey);
}

return instance;
}
}

export default FormatManager;

export { FormatManager };
87 changes: 87 additions & 0 deletions src/spec/FormatManager.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import FormatManager from '../lib/FormatManager';
import AssetManager from '../lib/AssetManager';
import { describe, expect, Mock, test, vi } from 'vitest';

const getMockAssetManager = () => {
const assetManager = new AssetManager('http://example.local', true);
assetManager.get = vi.fn();
return assetManager;
};

class SimpleFormat {
#simpleField: number;
#loaded = false;

constructor(simpleArg: number) {
this.#simpleField = simpleArg;
}

get simpleField() {
return this.#simpleField;
}

get loaded() {
return this.#loaded;
}

load(data: ArrayBuffer) {
this.#loaded = true;
return this;
}
}

describe('FormatManager', () => {
describe('get', () => {
test('should return new format instance when not previously loaded', async () => {
const mockAssetManager = getMockAssetManager();
const formatManager = new FormatManager(mockAssetManager);

const instance = await formatManager.get('foo', SimpleFormat, 7);

expect(mockAssetManager.get).toHaveBeenCalledWith('foo');
expect(instance).toBeInstanceOf(SimpleFormat);
expect(instance.simpleField).toBe(7);
expect(instance.loaded).toBe(true);
});

test('should return new format instance when previously loaded with different constructor args', async () => {
const mockAssetManager = getMockAssetManager();
const formatManager = new FormatManager(mockAssetManager);

const instance1 = await formatManager.get('foo', SimpleFormat, 7);
const instance2 = await formatManager.get('foo', SimpleFormat, 8);

expect(mockAssetManager.get).toHaveBeenCalledWith('foo');

expect(instance1).toBeInstanceOf(SimpleFormat);
expect(instance1.simpleField).toBe(7);
expect(instance1.loaded).toBe(true);

expect(instance2).toBeInstanceOf(SimpleFormat);
expect(instance2.simpleField).toBe(8);
expect(instance2.loaded).toBe(true);
expect(instance2 !== instance1).toBe(true);
});

test('should return existing format instance when previously loaded', async () => {
const mockAssetManager = getMockAssetManager();
const formatManager = new FormatManager(mockAssetManager);

const instance1 = await formatManager.get('foo', SimpleFormat, 7);
const instance2 = await formatManager.get('foo', SimpleFormat, 7);

expect(mockAssetManager.get).toHaveBeenCalledOnce();
expect(instance1).toBeInstanceOf(SimpleFormat);
expect(instance1).toBe(instance2);
});

test('should throw when asset data is not available', async () => {
const mockAssetManager = getMockAssetManager();
const formatManager = new FormatManager(mockAssetManager);

(mockAssetManager.get as Mock).mockRejectedValue(new Error('ohno'));

await expect(formatManager.get('foo', SimpleFormat, 7)).rejects.toBeInstanceOf(Error);
});
});
});

0 comments on commit 7d1873e

Please sign in to comment.