controller/FolderFlagSync.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | |||
| 3 | #include "controller/FolderContext.h" | ||
| 4 | #include "controller/PendingFlagIntents.h" | ||
| 5 | #include <QElapsedTimer> | ||
| 6 | #include <QHash> | ||
| 7 | #include <QList> | ||
| 8 | #include <QObject> | ||
| 9 | #include <QPair> | ||
| 10 | #include <QStringList> | ||
| 11 | #include <QThreadPool> | ||
| 12 | #include <atomic> | ||
| 13 | #include <functional> | ||
| 14 | #include <memory> | ||
| 15 | #include <optional> | ||
| 16 | |||
| 17 | class ImapService; | ||
| 18 | class MailCache; | ||
| 19 | class MailListModel; | ||
| 20 | class MailThreadModel; | ||
| 21 | class QTimer; | ||
| 22 | struct StatusResult; | ||
| 23 | |||
| 24 | // Live flag and count synchronisation of the selected folder (Sprint 91 | ||
| 25 | // S91.2.6, extracted from MailController): IDLE pushes, FETCH FLAGS / | ||
| 26 | // CONDSTORE snapshot reconciliation (small folders on the GUI thread, large | ||
| 27 | // ones on a read-only worker), STATUS polling of the subscribed folders and | ||
| 28 | // the IDLE/NOTIFY restart that ends a folder open. Callers reach it as | ||
| 29 | // `controller.flagSync()`. | ||
| 30 | // | ||
| 31 | // Owner: MailController (value member). GUI thread; the membership diff | ||
| 32 | // runs on the unit's two-thread pool with its own read-only cache | ||
| 33 | // connection. MailController connects the main connection's IDLE, flag and | ||
| 34 | // STATUS signals (and the search connection's STATUS signal) to the unit | ||
| 35 | // at the former positions, so receiver order is unchanged. The folder | ||
| 36 | // context, pending intents, subscribed folders and header-stream state stay | ||
| 37 | // owned by the controller (references / Deps); finishFlagSync — which | ||
| 38 | // continues the header-stream sequence — stays in the controller and is | ||
| 39 | // called through Deps. Controller signals keep their emitter (notifiers). | ||
| 40 | class FolderFlagSync final : public QObject { | ||
| 41 | Q_OBJECT | ||
| 42 | public: | ||
| 43 | struct Deps { | ||
| 44 | std::function<void()> ensureSearchConnection; | ||
| 45 | std::function<bool()> beginHeaderStream; | ||
| 46 | std::function<int()> pendingHeaderWrites; | ||
| 47 | std::function<void(const QString &)> failHeaderWrite; | ||
| 48 | // Keep the newest complete flag snapshot until the header writes settle. | ||
| 49 | std::function<void(const QList<QPair<qint64, quint32>> &)> deferHeaderFlags; | ||
| 50 | std::function<void()> resyncCurrentFolder; | ||
| 51 | std::function<void(QList<qint64>, bool)> finishFlagSync; | ||
| 52 | std::function<void()> requestFolderCountStatus; | ||
| 53 | // MailController signals (the emitter stays the controller). | ||
| 54 | std::function<void(const QString &)> statusMessage; | ||
| 55 | std::function<void(const QString &, int)> unreadCountChanged; | ||
| 56 | std::function<void(const QString &, int)> cachedUnreadCountChanged; | ||
| 57 | std::function<void(qint64, int, quint64)> authoritativeFolderPresented; | ||
| 58 | }; | ||
| 59 | FolderFlagSync(ImapService *imap, ImapService *const &searchImap, MailCache *cache, | ||
| 60 | MailListModel *model, MailThreadModel *const &threadModel, | ||
| 61 | const FolderContext &folder, PendingFlagIntents &flagIntents, | ||
| 62 | const QStringList &subscribedFolders, const bool &folderPresentationEnabled, | ||
| 63 | QElapsedTimer &folderSwitchStopwatch, Deps deps, QObject *parent = nullptr); | ||
| 64 | // Polling and expunge-debounce timers; called at the end of the | ||
| 65 | // controller constructor (their former position). | ||
| 66 | void connectTimers(); | ||
| 67 | |||
| 68 | // Quick's published indexed page can supply visible flags when the hidden | ||
| 69 | // flat model has no copy of those rows. Called only on the GUI thread. | ||
| 70 | 795 | void setVisibleIndexedFlagsProvider( | |
| 71 | std::function<QHash<qint64, quint32>(qint64)> provider) { | ||
| 72 | 795 | m_visibleIndexedFlags = std::move(provider); | |
| 73 | 795 | } | |
| 74 | // Sprint 49: Trigger immediate poll of all subscribed folders | ||
| 75 | void triggerPollNow(); | ||
| 76 | // Folder open finished its sync: NOTIFY or IDLE (plus STATUS polling). | ||
| 77 | void startIdleIfPossible(); | ||
| 78 | |||
| 79 | // Controller state transitions (folder/account switch, teardown). | ||
| 80 | void cancelMembershipRead(); // new sequence, cancel the running diff | ||
| 81 | void abortMembershipRead(); // cancel only (destructor) | ||
| 82 | void waitForMembershipReads(); | ||
| 83 | void stopPolling(); | ||
| 84 | void restartPolling(); // stop, then start when folders are subscribed | ||
| 85 | void resetPollingIndex(); | ||
| 86 | void clearChangedSinceFlags(); | ||
| 87 | |||
| 88 | // Observable state (tests and diagnostics). | ||
| 89 | bool expungeRefreshPending() const; | ||
| 90 | bool pollingActive() const; | ||
| 91 | 2 | bool initialPollDone() const { return m_initialPollDone; } | |
| 92 | 3 | int pollingIndex() const { return m_pollingIndex; } | |
| 93 | 4 | void setPollingIndex(int index) { m_pollingIndex = index; } | |
| 94 | |||
| 95 | public slots: | ||
| 96 | // IDLE event handlers | ||
| 97 | void onIdleNewMessages(int newCount); | ||
| 98 | void onIdleFlagsChanged(qint64 uid, quint32 flags); | ||
| 99 | void onIdleFlagsNeedRefetch(int seqNo); | ||
| 100 | void onIdleMessageExpunged(int seqNo); | ||
| 101 | // Flag sync result | ||
| 102 | void onFlagsReceived(const QList<QPair<qint64, quint32>> &uidFlags); | ||
| 103 | void onChangedFlagsReceived(const QList<QPair<qint64, quint32>> &uidFlags); | ||
| 104 | void onUidMembershipReceived(const QList<qint64> &uids); | ||
| 105 | // Folder status (polling) result | ||
| 106 | void onFolderStatusReceived(const StatusResult &result); | ||
| 107 | // F-26: a failed STATUS keeps the round going (failure isolation). | ||
| 108 | void onFolderStatusFailed(const QString &folder); | ||
| 109 | // Polling timer | ||
| 110 | void pollFolders(); | ||
| 111 | void pollNextFolder(); | ||
| 112 | |||
| 113 | private: | ||
| 114 | // `membership` set: CONDSTORE reopen (uidFlags holds only changed mail). | ||
| 115 | void reconcileLargeFlagSnapshot(const QList<QPair<qint64, quint32>> &uidFlags, | ||
| 116 | 6 | std::optional<QList<qint64>> membership = std::nullopt); | |
| 117 | bool idlePushBelongsToCurrentFolder() const; | ||
| 118 | |||
| 119 | ImapService *m_imap; | ||
| 120 | ImapService *const &m_searchImap; // owned by MailController (lazy) | ||
| 121 | MailCache *m_cache; | ||
| 122 | MailListModel *m_model; | ||
| 123 | MailThreadModel *const &m_threadModel; // set later on MailController | ||
| 124 | const FolderContext &m_folder; // MailController::m_folder | ||
| 125 | PendingFlagIntents &m_flagIntents; // MailMutationPipeline::intents() | ||
| 126 | const QStringList &m_subscribedFolders; // MailController::m_subscribedFolders | ||
| 127 | const bool &m_folderPresentationEnabled; | ||
| 128 | QElapsedTimer &m_folderSwitchStopwatch; // started by the folder switch | ||
| 129 | Deps m_deps; | ||
| 130 | QTimer *m_pollingTimer = nullptr; | ||
| 131 | int m_pollingIndex = 0; | ||
| 132 | bool m_initialPollDone = false; // T-066: initial poll on first IDLE start | ||
| 133 | // Debounce timer for IDLE expunge events (avoid N× fetchFlags for bulk deletes) | ||
| 134 | QTimer *m_expungeDebounceTimer = nullptr; | ||
| 135 | QThreadPool m_flagMembershipPool; | ||
| 136 | std::shared_ptr<std::atomic_bool> m_flagMembershipCancelled; | ||
| 137 | quint64 m_flagMembershipSequence = 0; | ||
| 138 | std::function<QHash<qint64, quint32>(qint64)> m_visibleIndexedFlags; | ||
| 139 | QList<QPair<qint64, quint32>> m_changedSinceFlags; | ||
| 140 | }; | ||
| 141 |