feat(storage): persist siprouter data in smartdata and smartbucket

This commit is contained in:
2026-05-21 23:35:50 +00:00
parent 04e706715f
commit 3e2fee16c1
14 changed files with 2018 additions and 492 deletions
+37 -41
View File
@@ -1,6 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import type { SiprouterStorage } from './storage.ts';
import type {
IFaxCompletedEvent,
IFaxFailedEvent,
@@ -16,6 +17,7 @@ export interface IFaxJob {
status: 'dialing' | 'started' | 'completed' | 'failed';
transport?: 'audio' | 't38';
filePath?: string;
objectKey?: string;
codec?: string;
remoteMedia?: string;
success?: boolean;
@@ -28,25 +30,21 @@ export interface IFaxJob {
}
export class FaxJobManager {
private readonly basePath: string;
private readonly jobsPath: string;
private jobs: IFaxJob[] = [];
private readonly log: (msg: string) => void;
private readonly storage: SiprouterStorage;
constructor(log: (msg: string) => void) {
this.basePath = path.join(process.cwd(), '.nogit', 'fax');
this.jobsPath = path.join(this.basePath, 'jobs.json');
constructor(log: (msg: string) => void, storageArg: SiprouterStorage) {
this.log = log;
this.storage = storageArg;
}
init(): void {
fs.mkdirSync(this.basePath, { recursive: true });
if (!fs.existsSync(this.jobsPath)) {
this.writeJobs([]);
}
async init(): Promise<void> {
this.jobs = await this.storage.getFaxJobs();
}
noteDialing(callId: string, number: string, providerId: string): void {
const jobs = this.loadJobs();
async noteDialing(callId: string, number: string, providerId: string): Promise<void> {
const jobs = this.jobs;
const now = Date.now();
const existing = jobs.find((job) => job.callId === callId);
if (existing) {
@@ -65,62 +63,61 @@ export class FaxJobManager {
updatedAt: now,
});
}
this.writeJobs(jobs);
await this.writeJobs();
}
noteStarted(event: IFaxStartedEvent): void {
const jobs = this.loadJobs();
async noteStarted(event: IFaxStartedEvent): Promise<void> {
const now = Date.now();
const job = this.getOrCreateJob(jobs, event.call_id, event.direction, now);
const job = this.getOrCreateJob(event.call_id, event.direction, now);
job.status = 'started';
job.transport = event.transport;
job.filePath = event.file_path;
await this.ensureOutboundFileObject(job, event.file_path);
job.codec = event.codec;
job.remoteMedia = event.remote_media;
job.updatedAt = now;
this.writeJobs(jobs);
await this.writeJobs();
}
noteCompleted(event: IFaxCompletedEvent): void {
const jobs = this.loadJobs();
async noteCompleted(event: IFaxCompletedEvent): Promise<void> {
const now = Date.now();
const job = this.getOrCreateJob(jobs, event.call_id, event.direction, now);
const job = this.getOrCreateJob(event.call_id, event.direction, now);
job.status = 'completed';
job.transport = event.transport;
job.filePath = event.file_path;
await this.ensureOutboundFileObject(job, 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);
await this.writeJobs();
}
noteFailed(event: IFaxFailedEvent): void {
const jobs = this.loadJobs();
async noteFailed(event: IFaxFailedEvent): Promise<void> {
const now = Date.now();
const job = this.getOrCreateJob(jobs, event.call_id, event.direction, now);
const job = this.getOrCreateJob(event.call_id, event.direction, now);
job.status = 'failed';
job.transport = event.transport;
job.filePath = event.file_path;
await this.ensureOutboundFileObject(job, event.file_path);
job.error = event.error;
job.success = false;
job.updatedAt = now;
this.writeJobs(jobs);
await this.writeJobs();
}
getJobs(): IFaxJob[] {
return this.loadJobs();
return [...this.jobs];
}
private getOrCreateJob(
jobs: IFaxJob[],
callId: string,
direction: 'outbound' | 'inbound',
now: number,
): IFaxJob {
let job = jobs.find((entry) => entry.callId === callId);
let job = this.jobs.find((entry) => entry.callId === callId);
if (!job) {
job = {
id: callId,
@@ -130,24 +127,23 @@ export class FaxJobManager {
createdAt: now,
updatedAt: now,
};
jobs.unshift(job);
this.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 async ensureOutboundFileObject(jobArg: IFaxJob, filePathArg: string | undefined): Promise<void> {
if (jobArg.direction !== 'outbound' || jobArg.objectKey || !filePathArg) return;
const localPath = path.isAbsolute(filePathArg) ? filePathArg : path.join(process.cwd(), filePathArg);
if (!fs.existsSync(localPath)) return;
const extension = path.extname(localPath) || '.tif';
jobArg.objectKey = await this.storage.putFileObject(`fax/outbound/${jobArg.callId}${extension}`, localPath);
}
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)`);
private async writeJobs(): Promise<void> {
await this.storage.writeFaxJobs(this.jobs);
this.log(`[fax] persisted ${this.jobs.length} job(s)`);
}
}