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
+100 -72
View File
@@ -8,7 +8,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { loadConfig, type IAppConfig } from './config.ts';
import { applyConfigUpdates, type IAppConfig } from './config.ts';
import { FaxBoxManager } from './faxbox.ts';
import { FaxJobManager } from './faxjobs.ts';
import { broadcastWs, initWebUi } from './frontend.ts';
@@ -27,24 +27,21 @@ import {
} from './proxybridge.ts';
import { registerProxyEventHandlers } from './runtime/proxy-events.ts';
import { StatusStore } from './runtime/status-store.ts';
import { SiprouterStorage } from './storage.ts';
import { WebRtcLinkManager, type IProviderMediaInfo } from './runtime/webrtc-linking.ts';
let appConfig: IAppConfig = loadConfig();
let appConfig: IAppConfig;
const LOG_PATH = path.join(process.cwd(), 'sip_trace.log');
const startTime = Date.now();
const instanceId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const statusStore = new StatusStore(appConfig);
const webRtcLinks = new WebRtcLinkManager();
const faxBoxManager = new FaxBoxManager(log);
const faxJobManager = new FaxJobManager(log);
const voiceboxManager = new VoiceboxManager(log);
faxBoxManager.init(appConfig.faxboxes ?? []);
faxJobManager.init();
voiceboxManager.init(appConfig.voiceboxes ?? []);
initWebRtcSignaling({ log });
const storage = new SiprouterStorage(log);
let statusStore: StatusStore;
let webRtcLinks: WebRtcLinkManager;
let faxBoxManager: FaxBoxManager;
let faxJobManager: FaxJobManager;
let voiceboxManager: VoiceboxManager;
function now(): string {
return new Date().toISOString().replace('T', ' ').slice(0, 19);
@@ -96,12 +93,12 @@ async function configureRuntime(config: IAppConfig): Promise<boolean> {
async function reloadConfig(): Promise<void> {
try {
const previousConfig = appConfig;
const nextConfig = loadConfig();
const nextConfig = await storage.getAppConfig();
appConfig = nextConfig;
statusStore.updateConfig(nextConfig);
faxBoxManager.init(nextConfig.faxboxes ?? []);
voiceboxManager.init(nextConfig.voiceboxes ?? []);
await faxBoxManager.init(nextConfig.faxboxes ?? []);
await voiceboxManager.init(nextConfig.voiceboxes ?? []);
if (nextConfig.proxy.lanPort !== previousConfig.proxy.lanPort) {
log('[config] proxy.lanPort changed; restart required for SIP socket rebinding');
@@ -121,6 +118,13 @@ async function reloadConfig(): Promise<void> {
}
}
async function updateConfig(updatesArg: any): Promise<IAppConfig> {
const nextConfig = applyConfigUpdates(appConfig, updatesArg);
await storage.writeAppConfig(nextConfig);
await reloadConfig();
return appConfig;
}
async function startProxyEngine(): Promise<void> {
const started = await initProxyEngine(log);
if (!started) {
@@ -155,77 +159,101 @@ async function startProxyEngine(): Promise<void> {
log(`proxy engine started | LAN ${appConfig.proxy.lanIp}:${appConfig.proxy.lanPort} | providers: ${providerList} | devices: ${deviceList}`);
}
initWebUi({
port: appConfig.proxy.webUiPort,
getStatus,
log,
onStartCall: async (number, deviceId, providerId) => {
log(`[dashboard] start call: ${number} device=${deviceId || 'any'} provider=${providerId || 'auto'}`);
const callId = await makeCall(number, deviceId, providerId);
if (!callId) {
log(`[dashboard] call failed for ${number}`);
return null;
}
log(`[dashboard] call started: ${callId}`);
statusStore.noteDashboardCallStarted(callId, number, providerId);
return { id: callId };
},
onHangupCall: (callId) => {
void hangupCall(callId);
return true;
},
onConfigSaved: reloadConfig,
faxBoxManager,
faxJobManager,
voiceboxManager,
onWebRtcOffer: async (sessionId, sdp, ws) => {
log(`[webrtc] offer from browser session=${sessionId.slice(0, 8)} sdp_type=${typeof sdp} sdp_len=${sdp?.length || 0}`);
if (!sdp || typeof sdp !== 'string' || sdp.length < 10) {
log(`[webrtc] WARNING: invalid SDP (type=${typeof sdp}), skipping offer`);
return;
}
async function main(): Promise<void> {
await storage.init();
appConfig = await storage.getAppConfig();
log(`[webrtc] sending offer to Rust (${sdp.length}b)...`);
const result = await webrtcOffer(sessionId, sdp);
log(`[webrtc] Rust result: ${JSON.stringify(result)?.slice(0, 200)}`);
if (result?.sdp) {
ws.send(JSON.stringify({ type: 'webrtc-answer', sessionId, sdp: result.sdp }));
log(`[webrtc] answer sent to browser session=${sessionId.slice(0, 8)}`);
return;
}
statusStore = new StatusStore(appConfig);
webRtcLinks = new WebRtcLinkManager();
faxBoxManager = new FaxBoxManager(log, storage);
faxJobManager = new FaxJobManager(log, storage);
voiceboxManager = new VoiceboxManager(log, storage);
log('[webrtc] ERROR: no answer SDP from Rust');
},
onWebRtcIce: async (sessionId, candidate) => {
await webrtcIce(sessionId, candidate as Parameters<typeof webrtcIce>[1]);
},
onWebRtcClose: async (sessionId) => {
webRtcLinks.removeSession(sessionId);
await webrtcClose(sessionId);
},
onWebRtcAccept: (callId, sessionId) => {
log(`[webrtc] accept: callId=${callId} sessionId=${sessionId.slice(0, 8)}`);
await faxBoxManager.init(appConfig.faxboxes ?? []);
await faxJobManager.init();
await voiceboxManager.init(appConfig.voiceboxes ?? []);
initWebRtcSignaling({ log });
const pendingMedia = webRtcLinks.acceptCall(callId, sessionId);
if (pendingMedia) {
requestWebRtcLink(callId, sessionId, pendingMedia);
return;
}
initWebUi({
port: appConfig.proxy.webUiPort,
getStatus,
getConfig: () => appConfig,
updateConfig,
log,
onStartCall: async (number, deviceId, providerId) => {
log(`[dashboard] start call: ${number} device=${deviceId || 'any'} provider=${providerId || 'auto'}`);
const callId = await makeCall(number, deviceId, providerId);
if (!callId) {
log(`[dashboard] call failed for ${number}`);
return null;
}
log(`[dashboard] call started: ${callId}`);
statusStore.noteDashboardCallStarted(callId, number, providerId);
return { id: callId };
},
onHangupCall: (callId) => {
void hangupCall(callId);
return true;
},
faxBoxManager,
faxJobManager,
voiceboxManager,
onWebRtcOffer: async (sessionId, sdp, ws) => {
log(`[webrtc] offer from browser session=${sessionId.slice(0, 8)} sdp_type=${typeof sdp} sdp_len=${sdp?.length || 0}`);
if (!sdp || typeof sdp !== 'string' || sdp.length < 10) {
log(`[webrtc] WARNING: invalid SDP (type=${typeof sdp}), skipping offer`);
return;
}
log(`[webrtc] session ${sessionId.slice(0, 8)} accepted, waiting for call_answered media info`);
},
log(`[webrtc] sending offer to Rust (${sdp.length}b)...`);
const result = await webrtcOffer(sessionId, sdp);
log(`[webrtc] Rust result: ${JSON.stringify(result)?.slice(0, 200)}`);
if (result?.sdp) {
ws.send(JSON.stringify({ type: 'webrtc-answer', sessionId, sdp: result.sdp }));
log(`[webrtc] answer sent to browser session=${sessionId.slice(0, 8)}`);
return;
}
log('[webrtc] ERROR: no answer SDP from Rust');
},
onWebRtcIce: async (sessionId, candidate) => {
await webrtcIce(sessionId, candidate as Parameters<typeof webrtcIce>[1]);
},
onWebRtcClose: async (sessionId) => {
webRtcLinks.removeSession(sessionId);
await webrtcClose(sessionId);
},
onWebRtcAccept: (callId, sessionId) => {
log(`[webrtc] accept: callId=${callId} sessionId=${sessionId.slice(0, 8)}`);
const pendingMedia = webRtcLinks.acceptCall(callId, sessionId);
if (pendingMedia) {
requestWebRtcLink(callId, sessionId, pendingMedia);
return;
}
log(`[webrtc] session ${sessionId.slice(0, 8)} accepted, waiting for call_answered media info`);
},
});
await startProxyEngine();
}
void main().catch((error) => {
log(`[FATAL] ${errorMessage(error)}`);
process.exit(1);
});
void startProxyEngine();
process.on('SIGINT', () => {
log('SIGINT, exiting');
shutdownProxyEngine();
void storage.close();
process.exit(0);
});
process.on('SIGTERM', () => {
log('SIGTERM, exiting');
shutdownProxyEngine();
void storage.close();
process.exit(0);
});