import type { IAssumptionCheckJob, IHttpCheckJob, ITcpCheckJob, IUptimeCheckResult, TCheckJob, TRunnerCheckResultStatus, } from './interfaces.ts'; const DEFAULT_TIMEOUT_MS = 10000; export class CheckExecutor { constructor(private readonly runnerId: string) {} public async execute(checkArg: TCheckJob): Promise { const timeStarted = Date.now(); try { const partialResult = await this.executeByType(checkArg, timeStarted); const timeEnded = Date.now(); return { checkId: checkArg.id, runnerId: this.runnerId, type: checkArg.type, timing: { timeStarted, timeEnded, duration: timeEnded - timeStarted, }, metadata: checkArg.metadata, ...partialResult, }; } catch (error) { const timeEnded = Date.now(); const isTimeout = error instanceof Error && /timed out|aborted/i.test(error.message); return { checkId: checkArg.id, runnerId: this.runnerId, type: checkArg.type, status: isTimeout ? 'timed out' : 'not ok', message: error instanceof Error ? error.message : String(error), timing: { timeStarted, timeEnded, duration: timeEnded - timeStarted, }, error: error instanceof Error ? error.message : String(error), metadata: checkArg.metadata, }; } } private async executeByType( checkArg: TCheckJob, timeStartedArg: number, ): Promise> { switch (checkArg.type) { case 'http': return await this.executeHttpCheck(checkArg, timeStartedArg); case 'tcp': return await this.executeTcpCheck(checkArg, timeStartedArg); case 'assumption': return this.executeAssumptionCheck(checkArg); default: { const neverCheck: never = checkArg; throw new Error(`Unsupported check type: ${JSON.stringify(neverCheck)}`); } } } private async executeHttpCheck( checkArg: IHttpCheckJob, timeStartedArg: number, ): Promise> { const controller = new AbortController(); const timeout = setTimeout( () => controller.abort('HTTP check timed out'), this.getTimeout(checkArg), ); const method = checkArg.method ?? (checkArg.expectedBodyIncludes ? 'GET' : 'HEAD'); try { const response = await fetch(checkArg.url, { method, headers: checkArg.headers, body: checkArg.body, signal: controller.signal, }); const expectedStatusCodes = checkArg.expectedStatusCodes ?? [200]; const statusMatches = expectedStatusCodes.includes(response.status); const body = checkArg.expectedBodyIncludes ? await response.text() : ''; const bodyMatches = checkArg.expectedBodyIncludes ? body.includes(checkArg.expectedBodyIncludes) : true; const status: TRunnerCheckResultStatus = statusMatches && bodyMatches ? 'ok' : 'not ok'; return { status, statusCode: response.status, responseTime: Date.now() - timeStartedArg, message: status === 'ok' ? `HTTP ${response.status} matched expectations` : `HTTP ${response.status} did not match expected status/body`, }; } finally { clearTimeout(timeout); } } private async executeTcpCheck( checkArg: ITcpCheckJob, timeStartedArg: number, ): Promise> { const timeoutMs = this.getTimeout(checkArg); let timeoutId: number | undefined; let connection: Deno.TcpConn | undefined; try { connection = await Promise.race([ Deno.connect({ hostname: checkArg.host, port: checkArg.port }), new Promise((_resolve, reject) => { timeoutId = setTimeout(() => reject(new Error('TCP check timed out')), timeoutMs); }), ]); return { status: 'ok', responseTime: Date.now() - timeStartedArg, message: `TCP connection established to ${checkArg.host}:${checkArg.port}`, }; } finally { if (timeoutId !== undefined) { clearTimeout(timeoutId); } connection?.close(); } } private executeAssumptionCheck( checkArg: IAssumptionCheckJob, ): Omit { return { status: checkArg.assumedStatus, message: checkArg.message ?? `Assumed status: ${checkArg.assumedStatus}`, }; } private getTimeout(checkArg: TCheckJob): number { return checkArg.timeoutMs ?? DEFAULT_TIMEOUT_MS; } }