perf(chat): ingest a message batch in one transaction

Ingesting a scan's worth of chat messages persisted each new message
with its own append() call, so a batch of fresh messages opened and
committed one SQLite transaction per row.

- A new appendBatch persists many messages in a single transaction,
  using the same INSERT OR IGNORE dedup key as append() and preserving
  input order so load()'s rowid ordering is unchanged.
- Every message is serialized and encrypted before the transaction
  opens, so a message with a non-UTF-8 body is skipped on its own
  without leaving a transaction open; the valid siblings still persist.
- Ingest collects the newly stored messages and writes them once after
  the loop instead of per message. It is inert (returns 0) while locked
  or on empty input.

Tests cover a batch insert, dedup on re-insert, empty input, bad-UTF-8
isolation with a valid sibling and a following append(), insert order,
and the locked no-op.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 22:03:00 -05:00
co-authored by Claude Opus 4.8
parent 1416654385
commit 3fd52fbdaf
4 changed files with 86 additions and 1 deletions
+3 -1
View File
@@ -38,6 +38,7 @@ int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata
const std::string myPubKey = chatIdentityPublicKeyHex(identity_);
int added = 0;
std::vector<ChatMessage> fresh; // persisted once after the loop (one transaction)
for (const auto& meta : metadata) {
// Already stored: append would refuse it. Every scan re-sends the whole history, so this skips the
// decrypt for all but the new messages.
@@ -107,13 +108,14 @@ int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata
// session loadFromDatabase() repopulates the store, so re-scanning the chain re-ingests but
// the store dedup prevents a duplicate write.
if (store_.append(message)) {
if (db_) db_->append(message);
fresh.push_back(message);
++added;
// Every ingested message is incoming — report its cid so the caller can notify without
// relying on a seen-watermark delta (which block-time vs wall-clock skew can swallow).
if (newIncomingCids) newIncomingCids->push_back(message.conversation_id);
}
}
if (db_ && !fresh.empty()) db_->appendBatch(fresh);
return added;
}