data/MutationJournalStore.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | |||
| 3 | #include <QByteArray> | ||
| 4 | #include <QList> | ||
| 5 | #include <QSet> | ||
| 6 | #include <QSqlDatabase> | ||
| 7 | #include <QString> | ||
| 8 | #include <functional> | ||
| 9 | #include <optional> | ||
| 10 | |||
| 11 | #include "data/MailRequest.h" | ||
| 12 | #include "data/MutationJournal.h" | ||
| 13 | |||
| 14 | // Durable mutation journal of the mail cache (Sprint 91 S91.3.2, extracted | ||
| 15 | // from MailCache): local intent → durable → dispatched → confirmed/failed. | ||
| 16 | // Callers reach it as `cache.journal()`. | ||
| 17 | // | ||
| 18 | // Plain class, no connection of its own: it uses the owning MailCache's | ||
| 19 | // QSqlDatabase and lastError string on that cache's thread (writer workers | ||
| 20 | // open their own MailCache). Every writer runs inside a ScopedWriteUnit: it | ||
| 21 | // joins a transaction the caller already holds (MailCache's flag/keyword | ||
| 22 | // writers, the selection move) and owns BEGIN IMMEDIATE otherwise, so the | ||
| 23 | // journal and its data change stay atomic (rule 6). | ||
| 24 | // enqueueMutationsInTransaction() never starts a transaction. Phase changes | ||
| 25 | // and removals are announced through notifiers bound to MailCache's | ||
| 26 | // mutationPhaseChanged / mutationRemoved signals. | ||
| 27 | class MutationJournalStore { | ||
| 28 | public: | ||
| 29 | struct Notify { | ||
| 30 | std::function<void(const QString &account, qint64 folderId, qint64 token, | ||
| 31 | qint64 uid, bool pending)> mutationPhaseChanged; | ||
| 32 | std::function<void(qint64 token)> mutationRemoved; | ||
| 33 | }; | ||
| 34 | MutationJournalStore(QSqlDatabase &db, QString &lastError, Notify notify); | ||
| 35 | |||
| 36 | // Persist before optimistic UI state changes. Desired-state operations for | ||
| 37 | // the same full identity + flag are atomically coalesced to the last intent. | ||
| 38 | // Returns the durable token, or nullopt when validation/backpressure/storage | ||
| 39 | // rejects the operation. | ||
| 40 | std::optional<qint64> enqueueMutation(const MutationRecord &record); | ||
| 41 | struct AppendPayload { | ||
| 42 | QByteArray message; | ||
| 43 | QString flags; | ||
| 44 | }; | ||
| 45 | static constexpr qint64 MaximumAppendMessageBytes = 80LL * 1024 * 1024; | ||
| 46 | static constexpr qint64 MaximumPendingAppendBytes = 256LL * 1024 * 1024; | ||
| 47 | // Journal intent and exact MIME bytes commit together. Confirmation/removal | ||
| 48 | // cascades to the payload; failed or ambiguous copies retain recovery data. | ||
| 49 | // A trusted builder may supply the SHA-256 of these exact immutable bytes | ||
| 50 | // so an already prepared payload need not be hashed on the GUI thread. | ||
| 51 | std::optional<qint64> enqueueAppendMutation(const MutationRecord &record, | ||
| 52 | const QByteArray &message, const QString &flags, | ||
| 53 | const QByteArray &precomputedSha256 = {}); | ||
| 54 | std::optional<AppendPayload> appendPayload(qint64 token, const QString &accountId, | ||
| 55 | QString *readError = nullptr) const; | ||
| 56 | qint64 appendPayloadBytes() const; | ||
| 57 | std::optional<QList<MutationRecord>> pendingAppendMutations(const QString &accountId, int limit = 100) const; | ||
| 58 | bool requeueFailedAppend(qint64 token, const QString &accountId); | ||
| 59 | // Persist one complete user action with a single capacity check and commit. | ||
| 60 | // The whole batch is rejected atomically; returned tokens retain input order. | ||
| 61 | std::optional<QList<qint64>> | ||
| 62 | enqueueMutations(const QList<MutationRecord> &records); | ||
| 63 | // Atomically reserve the exact queued intent before passing it to transport. | ||
| 64 | // A successful queued cancellation and a successful claim are exclusive. | ||
| 65 | bool claimMutationForDispatch(const MutationRecord &expected); | ||
| 66 | QList<MutationRecord> pendingMutations(const QString &accountId = {}) const; | ||
| 67 | std::optional<MutationRecord> mutation(qint64 token) const; | ||
| 68 | bool updateMutationPhase(qint64 token, MutationPhase phase, | ||
| 69 | const QString &error = {}, | ||
| 70 | const QString &uidPlusEvidence = {}); | ||
| 71 | // Removes the entry and announces it (mutationRemoved). A caller inside | ||
| 72 | // its own transaction passes `deferred`: the token is collected there and | ||
| 73 | // announced with announceRemoved() after the commit (S106-116), so a | ||
| 74 | // rolled-back removal is never published. | ||
| 75 | bool removeMutation(qint64 token, QList<qint64> *deferred = nullptr); | ||
| 76 | 2839 | void announceRemoved(qint64 token) const { m_notify.mutationRemoved(token); } | |
| 77 | // After a confirmed MOVE or authoritative absence of a removed source: | ||
| 78 | // retire its obsolete flags, preserving other identities/operation kinds. | ||
| 79 | QList<qint64> retireSourceFlagMutations(const MailId &source, quint32 uidValidity); | ||
| 80 | // 0 when the journal cannot be read; capacity checks use journalUsage(). | ||
| 81 | int mutationCount() const; | ||
| 82 | qint64 mutationJournalBytes() const; | ||
| 83 | // Entries and estimated bytes in one query; false when unreadable. | ||
| 84 | bool journalUsage(int *count, qint64 *bytes) const; | ||
| 85 | // P90.16: UIDs with an unresolved journaled move out of this folder. | ||
| 86 | // Projection loads exclude them so pending moves never re-appear in the | ||
| 87 | // visible list while the server confirmation is still outstanding. | ||
| 88 | QSet<qint64> pendingMoveUids(const QString &account, qint64 folderId, | ||
| 89 | bool *ok = nullptr) const; | ||
| 90 | bool hasPendingSeenMutation(const QString &account, qint64 folderId) const; | ||
| 91 | // Caller owns BEGIN IMMEDIATE, commit and rollback. | ||
| 92 | std::optional<QList<qint64>> enqueueMutationsInTransaction( | ||
| 93 | const QList<MutationRecord> &records, bool *capacityExceeded = nullptr); | ||
| 94 | |||
| 95 | // Backpressure limits (tests configure small journals through them). | ||
| 96 | 155 | int maxEntries() const { return m_mutationJournalMaxEntries; } | |
| 97 | 6566 | qint64 maxBytes() const { return m_mutationJournalMaxBytes; } | |
| 98 | 8 | void setMaxEntries(int entries) { m_mutationJournalMaxEntries = entries; } | |
| 99 | 5 | void setMaxBytes(qint64 bytes) { m_mutationJournalMaxBytes = bytes; } | |
| 100 | |||
| 101 | private: | ||
| 102 | QSqlDatabase &m_db; // owned by MailCache | ||
| 103 | QString &m_lastError; // MailCache::lastError() | ||
| 104 | Notify m_notify; | ||
| 105 | int m_mutationJournalMaxEntries = 5000; | ||
| 106 | qint64 m_mutationJournalMaxBytes = 2LL * 1024 * 1024; | ||
| 107 | }; | ||
| 108 |