-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
152 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |