import { performance } from 'node:perf_hooks';
import type { JokePreferences, Category } from '../validation.js';
import type { PrimaryProvider } from '../config.js';
import { logEvent, logError } from '../logger.js';
import * as jokeApiProvider from '../providers/jokeapi.js';
import * as officialProvider from '../providers/official.js';
import * as localProvider from '../providers/local.js';
import type { JokeProvider, ProviderName } from '../providers/types.js';
export interface JokeAgentConfig {
provider: PrimaryProvider;
timeoutMs: number;
retries: number;
allowNet: boolean;
verbose: boolean;
}
export interface JokeResponse {
text: string;
source: string;
category: Category;
latency_ms: number;
}
export interface JokeAttemptSummary {
provider: ProviderName;
message: string;
latency: number;
}
interface JokeAgentDependencies {
providers: Record<ProviderName, JokeProvider>;
logEvent: typeof logEvent;
logError: typeof logError;
now: () => number;
}
const defaultDependencies: JokeAgentDependencies = {
providers: {
jokeapi: jokeApiProvider,
official: officialProvider,
local: localProvider,
},
logEvent,
logError,
now: () => performance.now(),
};
export class JokeResolutionError extends Error {
constructor(message: string, public readonly details: JokeAttemptSummary[]) {
super(message);
this.name = 'JokeResolutionError';
}
}
export class JokeAgent {
private readonly providerChain: ProviderName[];
private readonly providers: Record<ProviderName, JokeProvider>;
private readonly infoLog: typeof logEvent;
private readonly errorLog: typeof logError;
private readonly now: () => number;
constructor(private readonly config: JokeAgentConfig, dependencies: JokeAgentDependencies = defaultDependencies) {
this.providerChain = buildProviderChain(config.provider);
this.providers = dependencies.providers;
this.infoLog = config.verbose ? dependencies.logEvent : noop;
this.errorLog = config.verbose ? dependencies.logError : noop;
this.now = dependencies.now;
}
async resolve(preferences: JokePreferences): Promise<JokeResponse> {
const attempts: JokeAttemptSummary[] = [];
for (const providerName of this.providerChain) {
const start = this.now();
if (!this.config.allowNet && providerName !== 'local') {
const latency = Math.round(this.now() - start);
const message = 'Network access disabled';
attempts.push({ provider: providerName, message, latency });
this.infoLog({
event: 'provider_skipped',
provider: providerName,
reason: message,
latency_ms: latency,
fallback_depth: attempts.length,
});
continue;
}
try {
const provider = this.providers[providerName];
if (!provider) {
throw new Error(`Unknown provider ${providerName}`);
}
const result = await provider.getJoke(preferences, {
timeoutMs: this.config.timeoutMs,
retries: this.config.retries,
allowNet: this.config.allowNet,
});
const latency = Math.round(this.now() - start);
const response: JokeResponse = {
text: result.text,
source: result.source ?? providerName,
category: (result.category ?? preferences.category) as Category,
latency_ms: latency,
};
this.infoLog({
event: 'getJoke',
provider: response.source,
latency_ms: latency,
fallback_depth: attempts.length,
});
return response;
} catch (error) {
const latency = Math.round(this.now() - start);
const message = error instanceof Error ? error.message : String(error);
attempts.push({ provider: providerName, message, latency });
this.errorLog(error, { event: 'provider_failure', provider: providerName, latency_ms: latency });
}
}
throw new JokeResolutionError('All joke providers failed', attempts);
}
}
export function createJokeAgent(
config: JokeAgentConfig,
dependencies: Partial<JokeAgentDependencies> = {},
): JokeAgent {
const resolvedDependencies: JokeAgentDependencies = {
providers: dependencies.providers ?? defaultDependencies.providers,
logEvent: dependencies.logEvent ?? defaultDependencies.logEvent,
logError: dependencies.logError ?? defaultDependencies.logError,
now: dependencies.now ?? defaultDependencies.now,
};
return new JokeAgent(config, resolvedDependencies);
}
export function buildProviderChain(primary: PrimaryProvider): ProviderName[] {
if (primary === 'jokeapi') {
return ['jokeapi', 'official', 'local'];
}
return ['official', 'jokeapi', 'local'];
}
function noop(): void {}