Files
ObsidianDragon/src/chat/chat_store.cpp
T
DanSandClaude Opus 4.8 edbb98d9ec perf(chat): single-pass watermark and move-append on chat load
Loading chat history did more work than it needed to. Seeding the unread
watermark walked every conversation and copied each one's full message
list out of the store just to read the last timestamp, and the database
reload copied every message into the store one at a time with no
reservation.

- The watermark now comes from one pass over the stored messages that
  returns the latest timestamp per conversation, with no per-
  conversation copy or sort.
- The store gained a move-append and a reserve, so the reload pre-sizes
  its vector and dedup set once and moves each row in instead of copying
  it. The dedup key is computed before the move.

Behaviour is unchanged: the watermark values and the blocked-conversation
filter on reload are the same as before.

Tests cover the per-conversation latest-timestamp pass and move-append.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 19:24:12 -05:00

120 lines
4.2 KiB
C++

// DragonX Wallet - HushChat in-memory message store (implementation).
#include "chat_store.h"
#include <algorithm>
namespace dragonx::chat {
std::string ChatStore::messageKey(const std::string& txid, std::size_t payloadPosition) {
return txid + ":" + std::to_string(payloadPosition);
}
std::string ChatStore::dedupKey(const ChatMessage& message) {
return messageKey(message.txid, message.payload_position);
}
bool ChatStore::append(const ChatMessage& message) {
if (!seen_.insert(dedupKey(message)).second) return false;
messages_.push_back(message);
++revision_;
return true;
}
bool ChatStore::append(ChatMessage&& message) {
if (!seen_.insert(dedupKey(message)).second) return false; // reads txid/payload_position before the move
messages_.push_back(std::move(message));
++revision_;
return true;
}
void ChatStore::reserve(std::size_t n) {
messages_.reserve(n);
seen_.reserve(n);
}
std::unordered_map<std::string, std::int64_t> ChatStore::latestTimestampByConversation() const {
std::unordered_map<std::string, std::int64_t> out;
for (const auto& message : messages_) {
auto it = out.find(message.conversation_id);
if (it == out.end()) out.emplace(message.conversation_id, static_cast<std::int64_t>(message.timestamp));
else if (static_cast<std::int64_t>(message.timestamp) > it->second) it->second = static_cast<std::int64_t>(message.timestamp);
}
return out;
}
std::vector<ChatMessage> ChatStore::conversation(const std::string& conversationId) const {
std::vector<ChatMessage> out;
for (const auto& message : messages_) {
if (message.conversation_id == conversationId) out.push_back(message);
}
// Return chronological (oldest→newest). messages_ is in scan/insertion order, which is NOT
// time-ordered (a full scan harvests txids in std::set/unordered_map order) — that would mis-order the
// rendered thread AND let the reply-target pin (first-seen peer address) latch onto a non-establishing
// message (B2). timestamp is the block/tx time (peer can't set it); tie-break txid + payload_position
// for determinism.
std::stable_sort(out.begin(), out.end(), [](const ChatMessage& a, const ChatMessage& b) {
if (a.timestamp != b.timestamp) return a.timestamp < b.timestamp;
if (a.txid != b.txid) return a.txid < b.txid;
return a.payload_position < b.payload_position;
});
return out;
}
const ChatMessage* ChatStore::updateDelivery(const std::string& txid, ChatDelivery delivery) {
for (auto& message : messages_) {
if (message.txid == txid) {
message.delivery = delivery;
++revision_;
return &message;
}
}
return nullptr;
}
int ChatStore::countUnread(const std::function<bool(const std::string&)>& excluded,
const std::function<std::int64_t(const std::string&)>& seenFor) const {
int unread = 0;
for (const auto& m : messages_) {
if (m.direction != ChatDirection::Incoming) continue;
if (excluded && excluded(m.conversation_id)) continue;
if (m.timestamp > seenFor(m.conversation_id)) ++unread;
}
return unread;
}
std::vector<std::string> ChatStore::conversationIds() const {
std::vector<std::string> ids;
std::unordered_set<std::string> seenIds;
for (const auto& message : messages_) {
if (seenIds.insert(message.conversation_id).second) ids.push_back(message.conversation_id);
}
return ids;
}
std::vector<ChatMessage> ChatStore::eraseConversation(const std::string& conversationId) {
std::vector<ChatMessage> removed;
std::vector<ChatMessage> kept;
kept.reserve(messages_.size());
for (auto& m : messages_) {
if (m.conversation_id == conversationId) {
seen_.erase(dedupKey(m));
removed.push_back(std::move(m));
} else {
kept.push_back(std::move(m));
}
}
messages_.swap(kept);
if (!removed.empty()) ++revision_;
return removed;
}
void ChatStore::clear() {
messages_.clear();
seen_.clear();
txid_index_.clear(); // drop the History-badge txid copies too (revision_ below invalidates the memo)
++revision_;
}
} // namespace dragonx::chat