import fs from 'node:fs'; import path from 'node:path'; import type { IFaxCompletedEvent, IFaxFailedEvent, IFaxStartedEvent, } from './shared/proxy-events.ts'; export interface IFaxJob { id: string; callId: string; number?: string; providerId?: string; direction: 'outbound' | 'inbound'; status: 'dialing' | 'started' | 'completed' | 'failed'; transport?: 'audio' | 't38'; filePath?: string; codec?: string; remoteMedia?: string; success?: boolean; completionCode?: number | null; completionLabel?: string | null; error?: string; stats?: IFaxCompletedEvent['stats']; createdAt: number; updatedAt: number; } export class FaxJobManager { private readonly basePath: string; private readonly jobsPath: string; private readonly log: (msg: string) => void; constructor(log: (msg: string) => void) { this.basePath = path.join(process.cwd(), '.nogit', 'fax'); this.jobsPath = path.join(this.basePath, 'jobs.json'); this.log = log; } init(): void { fs.mkdirSync(this.basePath, { recursive: true }); if (!fs.existsSync(this.jobsPath)) { this.writeJobs([]); } } noteDialing(callId: string, number: string, providerId: string): void { const jobs = this.loadJobs(); const now = Date.now(); const existing = jobs.find((job) => job.callId === callId); if (existing) { existing.number = number; existing.providerId = providerId; existing.updatedAt = now; } else { jobs.unshift({ id: callId, callId, number, providerId, direction: 'outbound', status: 'dialing', createdAt: now, updatedAt: now, }); } this.writeJobs(jobs); } noteStarted(event: IFaxStartedEvent): void { const jobs = this.loadJobs(); const now = Date.now(); const job = this.getOrCreateJob(jobs, event.call_id, event.direction, now); job.status = 'started'; job.transport = event.transport; job.filePath = event.file_path; job.codec = event.codec; job.remoteMedia = event.remote_media; job.updatedAt = now; this.writeJobs(jobs); } noteCompleted(event: IFaxCompletedEvent): void { const jobs = this.loadJobs(); const now = Date.now(); const job = this.getOrCreateJob(jobs, event.call_id, event.direction, now); job.status = 'completed'; job.transport = event.transport; job.filePath = event.file_path; job.codec = event.codec; job.success = event.success; job.completionCode = event.completion_code ?? null; job.completionLabel = event.completion_label ?? null; job.stats = event.stats; job.updatedAt = now; this.writeJobs(jobs); } noteFailed(event: IFaxFailedEvent): void { const jobs = this.loadJobs(); const now = Date.now(); const job = this.getOrCreateJob(jobs, event.call_id, event.direction, now); job.status = 'failed'; job.transport = event.transport; job.filePath = event.file_path; job.error = event.error; job.success = false; job.updatedAt = now; this.writeJobs(jobs); } getJobs(): IFaxJob[] { return this.loadJobs(); } private getOrCreateJob( jobs: IFaxJob[], callId: string, direction: 'outbound' | 'inbound', now: number, ): IFaxJob { let job = jobs.find((entry) => entry.callId === callId); if (!job) { job = { id: callId, callId, direction, status: 'dialing', createdAt: now, updatedAt: now, }; jobs.unshift(job); } return job; } private loadJobs(): IFaxJob[] { try { const content = fs.readFileSync(this.jobsPath, 'utf8'); const parsed = JSON.parse(content); return Array.isArray(parsed) ? parsed as IFaxJob[] : []; } catch { return []; } } private writeJobs(jobs: IFaxJob[]): void { fs.mkdirSync(this.basePath, { recursive: true }); fs.writeFileSync(this.jobsPath, JSON.stringify(jobs, null, 2)); this.log(`[fax] persisted ${jobs.length} job(s)`); } }