Files
gridpilot.gg/adapters/media/MediaResolverInMemoryAdapter.ts
2026-01-16 21:40:26 +01:00

229 lines
6.0 KiB
TypeScript

/**
* In-Memory Media Resolver Adapter
*
* Stub implementation for testing purposes.
* Resolves MediaReference objects to fake URLs without external dependencies.
*
* Part of the adapters layer, implementing the MediaResolverPort interface.
*/
import { MediaReference } from '@core/domain/media/MediaReference';
import { MediaResolverPort } from '@core/ports/media/MediaResolverPort';
/**
* Configuration for InMemoryMediaResolverAdapter
*/
export interface InMemoryMediaResolverConfig {
/**
* Base URL to use for generated URLs
* @default 'https://fake-media.example.com'
*/
baseUrl?: string;
/**
* Whether to simulate network delays
* @default false
*/
simulateDelay?: boolean;
/**
* Delay in milliseconds when simulateDelay is true
* @default 50
*/
delayMs?: number;
/**
* Whether to return null for certain reference types (simulating missing media)
* @default false
*/
simulateMissingMedia?: boolean;
}
/**
* In-Memory Media Resolver Adapter
*
* Stub implementation that resolves media references to fake URLs.
* Designed for use in tests and development environments.
*
* @example
* ```typescript
* const adapter = new InMemoryMediaResolverAdapter({
* baseUrl: 'https://test.example.com',
* simulateDelay: true
* });
*
* const ref = MediaReference.createSystemDefault('avatar');
* const url = await adapter.resolve(ref);
* // Returns: '/media/default/male-default-avatar.png'
* ```
*/
export class InMemoryMediaResolverAdapter implements MediaResolverPort {
private readonly config: Required<InMemoryMediaResolverConfig>;
constructor(config: InMemoryMediaResolverConfig = {}) {
this.config = {
baseUrl: config.baseUrl ?? 'https://fake-media.example.com',
simulateDelay: config.simulateDelay ?? false,
delayMs: config.delayMs ?? 50,
simulateMissingMedia: config.simulateMissingMedia ?? false,
};
}
/**
* Resolve a media reference to a path-only URL
*
* @param ref - The media reference to resolve
* @returns Promise resolving to path string or null
*/
async resolve(ref: MediaReference): Promise<string | null> {
// Simulate network delay if configured
if (this.config.simulateDelay) {
await this.delay(this.config.delayMs);
}
// Simulate missing media for some cases
if (this.config.simulateMissingMedia && this.shouldReturnNull()) {
return null;
}
switch (ref.type) {
case 'system-default':
let filename: string;
if (ref.variant === 'avatar' && ref.avatarVariant) {
filename = `${ref.avatarVariant}-default-avatar.png`;
} else if (ref.variant === 'avatar') {
filename = `neutral-default-avatar.png`;
} else {
filename = `${ref.variant}.png`;
}
return `/media/default/${filename}`;
case 'generated':
// Parse the generationRequestId to extract type and id
// Format: "{type}-{id}" where id can contain hyphens
if (ref.generationRequestId) {
const firstHyphenIndex = ref.generationRequestId.indexOf('-');
if (firstHyphenIndex !== -1) {
const type = ref.generationRequestId.substring(0, firstHyphenIndex);
const id = ref.generationRequestId.substring(firstHyphenIndex + 1);
// Use the correct API routes
if (type === 'team') {
return `/media/teams/${id}/logo`;
} else if (type === 'league') {
return `/media/leagues/${id}/logo`;
} else if (type === 'driver') {
return `/media/avatar/${id}`;
}
// Fallback for other types
return `/media/generated/${type}/${id}`;
}
}
// Fallback for unexpected format
return null;
case 'uploaded':
return `/media/uploaded/${ref.mediaId}`;
case 'none':
return null;
default:
return null;
}
}
/**
* Simulate network delay
*/
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Determine if this reference should return null (simulating missing media)
*/
private shouldReturnNull(): boolean {
// Randomly return null for 20% of cases
return Math.random() < 0.2;
}
/**
* Get the configured base URL
*/
getBaseUrl(): string {
return this.config.baseUrl;
}
/**
* Update configuration
*/
updateConfig(config: Partial<InMemoryMediaResolverConfig>): void {
Object.assign(this.config, config);
}
/**
* Reset to default configuration
*/
reset(): void {
this.config.baseUrl = 'https://fake-media.example.com';
this.config.simulateDelay = false;
this.config.delayMs = 50;
this.config.simulateMissingMedia = false;
}
}
/**
* Factory function to create a configured in-memory resolver
*/
export function createInMemoryResolver(
config: InMemoryMediaResolverConfig = {}
): MediaResolverPort {
return new InMemoryMediaResolverAdapter(config);
}
/**
* Pre-configured resolver for common test scenarios
*/
export const TestResolvers = {
/**
* Fast resolver with no delays
*/
fast: () => new InMemoryMediaResolverAdapter({
baseUrl: 'https://test.example.com',
simulateDelay: false,
}),
/**
* Slow resolver that simulates network latency
*/
slow: () => new InMemoryMediaResolverAdapter({
baseUrl: 'https://test.example.com',
simulateDelay: true,
delayMs: 200,
}),
/**
* Unreliable resolver that sometimes returns null
*/
unreliable: () => new InMemoryMediaResolverAdapter({
baseUrl: 'https://test.example.com',
simulateMissingMedia: true,
}),
/**
* Custom base URL resolver
*/
withBaseUrl: (baseUrl: string) => new InMemoryMediaResolverAdapter({
baseUrl,
simulateDelay: false,
}),
/**
* Local development resolver
*/
local: () => new InMemoryMediaResolverAdapter({
baseUrl: 'http://localhost:3000/media',
simulateDelay: false,
}),
} as const;