134 lines
4.2 KiB
TypeScript
134 lines
4.2 KiB
TypeScript
|
|
import { executeSql, querySql } from '@/core/storage/sqlite';
|
||
|
|
|
||
|
|
import type { SegmentOutboxEntry, SegmentOutboxStatus } from './types';
|
||
|
|
|
||
|
|
const CREATE_TABLE_SQL = `
|
||
|
|
CREATE TABLE IF NOT EXISTS segment_outbox (
|
||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
conversation_id TEXT NOT NULL,
|
||
|
|
voice_session_id TEXT NOT NULL,
|
||
|
|
segment_index INTEGER NOT NULL,
|
||
|
|
file_uri TEXT NOT NULL,
|
||
|
|
duration_ms INTEGER NOT NULL,
|
||
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
||
|
|
retry_count INTEGER NOT NULL DEFAULT 0,
|
||
|
|
created_at INTEGER NOT NULL
|
||
|
|
);
|
||
|
|
`;
|
||
|
|
|
||
|
|
let initialized = false;
|
||
|
|
|
||
|
|
async function ensureTable(): Promise<void> {
|
||
|
|
if (initialized) return;
|
||
|
|
await executeSql(CREATE_TABLE_SQL);
|
||
|
|
initialized = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
function mapRow(row: Record<string, unknown>): SegmentOutboxEntry {
|
||
|
|
return {
|
||
|
|
id: row.id as number,
|
||
|
|
conversationId: row.conversation_id as string,
|
||
|
|
voiceSessionId: row.voice_session_id as string,
|
||
|
|
segmentIndex: row.segment_index as number,
|
||
|
|
fileUri: row.file_uri as string,
|
||
|
|
durationMs: row.duration_ms as number,
|
||
|
|
status: row.status as SegmentOutboxStatus,
|
||
|
|
retryCount: row.retry_count as number,
|
||
|
|
createdAt: row.created_at as number,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* SQLite-backed outbox for voice segments.
|
||
|
|
* Stores metadata + queue state only — audio files live on the local filesystem.
|
||
|
|
*
|
||
|
|
* State machine: pending → sending → sent | failed
|
||
|
|
* Single writer pattern enforced by serializing all writes through this module.
|
||
|
|
*/
|
||
|
|
export const segmentOutbox = {
|
||
|
|
async enqueue(
|
||
|
|
entry: Omit<
|
||
|
|
SegmentOutboxEntry,
|
||
|
|
'id' | 'status' | 'retryCount' | 'createdAt'
|
||
|
|
>,
|
||
|
|
): Promise<number> {
|
||
|
|
await ensureTable();
|
||
|
|
const result = await executeSql(
|
||
|
|
`INSERT INTO segment_outbox (conversation_id, voice_session_id, segment_index, file_uri, duration_ms, status, retry_count, created_at)
|
||
|
|
VALUES (?, ?, ?, ?, ?, 'pending', 0, ?)`,
|
||
|
|
[
|
||
|
|
entry.conversationId,
|
||
|
|
entry.voiceSessionId,
|
||
|
|
entry.segmentIndex,
|
||
|
|
entry.fileUri,
|
||
|
|
entry.durationMs,
|
||
|
|
Date.now(),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
return result.lastInsertRowId;
|
||
|
|
},
|
||
|
|
|
||
|
|
async getPending(conversationId?: string): Promise<SegmentOutboxEntry[]> {
|
||
|
|
await ensureTable();
|
||
|
|
const sql = conversationId
|
||
|
|
? `SELECT * FROM segment_outbox WHERE status = 'pending' AND conversation_id = ? ORDER BY created_at ASC`
|
||
|
|
: `SELECT * FROM segment_outbox WHERE status = 'pending' ORDER BY created_at ASC`;
|
||
|
|
const params = conversationId ? [conversationId] : [];
|
||
|
|
const rows = await querySql<Record<string, unknown>>(sql, params);
|
||
|
|
return rows.map(mapRow);
|
||
|
|
},
|
||
|
|
|
||
|
|
async markSending(id: number): Promise<void> {
|
||
|
|
await ensureTable();
|
||
|
|
await executeSql(
|
||
|
|
`UPDATE segment_outbox SET status = 'sending' WHERE id = ?`,
|
||
|
|
[id],
|
||
|
|
);
|
||
|
|
},
|
||
|
|
|
||
|
|
async markSent(id: number): Promise<void> {
|
||
|
|
await ensureTable();
|
||
|
|
await executeSql(`UPDATE segment_outbox SET status = 'sent' WHERE id = ?`, [
|
||
|
|
id,
|
||
|
|
]);
|
||
|
|
},
|
||
|
|
|
||
|
|
async markFailed(id: number): Promise<void> {
|
||
|
|
await ensureTable();
|
||
|
|
await executeSql(
|
||
|
|
`UPDATE segment_outbox SET status = 'failed', retry_count = retry_count + 1 WHERE id = ?`,
|
||
|
|
[id],
|
||
|
|
);
|
||
|
|
},
|
||
|
|
|
||
|
|
async resetFailed(conversationId?: string): Promise<void> {
|
||
|
|
await ensureTable();
|
||
|
|
const sql = conversationId
|
||
|
|
? `UPDATE segment_outbox SET status = 'pending' WHERE status = 'failed' AND conversation_id = ?`
|
||
|
|
: `UPDATE segment_outbox SET status = 'pending' WHERE status = 'failed'`;
|
||
|
|
const params = conversationId ? [conversationId] : [];
|
||
|
|
await executeSql(sql, params);
|
||
|
|
},
|
||
|
|
|
||
|
|
async clearSent(conversationId?: string): Promise<void> {
|
||
|
|
await ensureTable();
|
||
|
|
const sql = conversationId
|
||
|
|
? `DELETE FROM segment_outbox WHERE status = 'sent' AND conversation_id = ?`
|
||
|
|
: `DELETE FROM segment_outbox WHERE status = 'sent'`;
|
||
|
|
const params = conversationId ? [conversationId] : [];
|
||
|
|
await executeSql(sql, params);
|
||
|
|
},
|
||
|
|
|
||
|
|
async clearAll(conversationId: string): Promise<void> {
|
||
|
|
await ensureTable();
|
||
|
|
await executeSql(`DELETE FROM segment_outbox WHERE conversation_id = ?`, [
|
||
|
|
conversationId,
|
||
|
|
]);
|
||
|
|
},
|
||
|
|
|
||
|
|
/** For testing — reset the initialization flag. */
|
||
|
|
_resetForTest(): void {
|
||
|
|
initialized = false;
|
||
|
|
},
|
||
|
|
} as const;
|