feat(sdk): conversation management — DM/group lifecycle, outbox

This commit is contained in:
2026-03-04 12:39:48 +01:00
parent 67983c7a40
commit 4d62a837a5
4 changed files with 541 additions and 0 deletions

View File

@@ -298,6 +298,78 @@ impl ConversationStore {
Ok(())
}
/// Enqueue an outbox entry for offline sending.
pub fn enqueue_outbox(
&self,
conv_id: &ConversationId,
recipient_key: &[u8],
payload: &[u8],
) -> anyhow::Result<()> {
self.conn.execute(
"INSERT INTO outbox (conversation_id, recipient_key, payload, created_at_ms)
VALUES (?1, ?2, ?3, ?4)",
params![conv_id.0.as_slice(), recipient_key, payload, now_ms() as i64],
)?;
Ok(())
}
/// Load all pending outbox entries, oldest first.
pub fn load_pending_outbox(&self) -> anyhow::Result<Vec<OutboxEntry>> {
let mut stmt = self.conn.prepare(
"SELECT id, conversation_id, recipient_key, payload, retry_count
FROM outbox WHERE status = 'pending' ORDER BY created_at_ms",
)?;
let rows = stmt.query_map([], |row| {
let id: i64 = row.get(0)?;
let conv_blob: Vec<u8> = row.get(1)?;
let recipient_key: Vec<u8> = row.get(2)?;
let payload: Vec<u8> = row.get(3)?;
let retry_count: u32 = row.get(4)?;
Ok(OutboxEntry {
id,
conversation_id: ConversationId::from_slice(&conv_blob)
.unwrap_or(ConversationId([0; 16])),
recipient_key,
payload,
retry_count,
})
})?;
let mut entries = Vec::new();
for row in rows {
entries.push(row?);
}
Ok(entries)
}
/// Mark an outbox entry as sent.
pub fn mark_outbox_sent(&self, id: i64) -> anyhow::Result<()> {
self.conn.execute(
"UPDATE outbox SET status = 'sent' WHERE id = ?1",
params![id],
)?;
Ok(())
}
/// Mark an outbox entry as failed (retryable up to 5 times).
pub fn mark_outbox_failed(&self, id: i64, retry_count: u32) -> anyhow::Result<()> {
let new_status = if retry_count > 5 { "failed" } else { "pending" };
self.conn.execute(
"UPDATE outbox SET retry_count = ?2, status = ?3 WHERE id = ?1",
params![id, retry_count, new_status],
)?;
Ok(())
}
/// Count pending outbox entries.
pub fn count_pending_outbox(&self) -> anyhow::Result<usize> {
let count: i64 = self.conn.query_row(
"SELECT COUNT(*) FROM outbox WHERE status = 'pending'",
[],
|row| row.get(0),
)?;
Ok(count as usize)
}
/// Load recent messages (newest first, then reversed to chronological).
pub fn load_recent_messages(
&self,
@@ -454,6 +526,14 @@ fn to_16(v: &[u8]) -> Option<[u8; 16]> {
}
}
/// Current timestamp in milliseconds since UNIX epoch.
pub fn now_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
fn row_to_message(
conv_id: &ConversationId,
row: &rusqlite::Row<'_>,