controller/FolderFlagSync.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "controller/FolderFlagSync.h" | ||
| 2 | |||
| 3 | #include "data/MailCache.h" | ||
| 4 | #include "performance/BackgroundPerformanceTracker.h" | ||
| 5 | #include "performance/FolderPerformanceTracker.h" | ||
| 6 | #include "service/ImapService.h" | ||
| 7 | #include "ui/MailListModel.h" | ||
| 8 | #include "ui/MailThreadModel.h" | ||
| 9 | #include <QFutureWatcher> | ||
| 10 | #include <QLoggingCategory> | ||
| 11 | #include <QPointer> | ||
| 12 | #include <QTimer> | ||
| 13 | #include <QtConcurrentRun> | ||
| 14 | |||
| 15 | // Same category as MailController (defined there), see docs/LOGGING.md. | ||
| 16 | Q_DECLARE_LOGGING_CATEGORY(lcController) | ||
| 17 | |||
| 18 | static constexpr int POLLING_INTERVAL_MS = 60 * 1000; // 60 seconds | ||
| 19 | |||
| 20 | 1050 | FolderFlagSync::FolderFlagSync(ImapService *imap, ImapService *const &searchImap, | |
| 21 | MailCache *cache, MailListModel *model, | ||
| 22 | MailThreadModel *const &threadModel, const FolderContext &folder, | ||
| 23 | PendingFlagIntents &flagIntents, | ||
| 24 | const QStringList &subscribedFolders, | ||
| 25 | const bool &folderPresentationEnabled, | ||
| 26 | 1050 | QElapsedTimer &folderSwitchStopwatch, Deps deps, QObject *parent) | |
| 27 | 1050 | : QObject(parent), m_imap(imap), m_searchImap(searchImap), m_cache(cache), m_model(model), | |
| 28 | 1050 | m_threadModel(threadModel), m_folder(folder), m_flagIntents(flagIntents), | |
| 29 | 1050 | m_subscribedFolders(subscribedFolders), | |
| 30 | 1050 | m_folderPresentationEnabled(folderPresentationEnabled), | |
| 31 | 1050 | m_folderSwitchStopwatch(folderSwitchStopwatch), m_deps(std::move(deps)), | |
| 32 |
4/7✓ Branch 5 → 6 taken 1050 times.
✓ Branch 6 → 7 taken 1050 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 1050 times.
✓ Branch 9 → 10 taken 1050 times.
✗ Branch 15 → 16 not taken.
✗ Branch 15 → 17 not taken.
|
2100 | m_pollingTimer(new QTimer(this)) { |
| 33 |
1/1✓ Branch 13 → 14 taken 1050 times.
|
1050 | m_flagMembershipPool.setMaxThreadCount(2); |
| 34 | 1050 | } | |
| 35 | |||
| 36 | // Formerly the end of the MailController constructor. | ||
| 37 | 1050 | void FolderFlagSync::connectTimers() { | |
| 38 | // Polling timer | ||
| 39 | 1050 | m_pollingTimer->setInterval(POLLING_INTERVAL_MS); | |
| 40 |
1/1✓ Branch 3 → 4 taken 1050 times.
|
1050 | connect(m_pollingTimer, &QTimer::timeout, this, &FolderFlagSync::pollFolders); |
| 41 | |||
| 42 | // Expunge debounce timer: coalesces rapid IDLE expunge events into one fetchFlags | ||
| 43 |
2/5✓ Branch 6 → 7 taken 1050 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 1050 times.
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 18 not taken.
|
1050 | m_expungeDebounceTimer = new QTimer(this); |
| 44 | 1050 | m_expungeDebounceTimer->setSingleShot(true); | |
| 45 | 1050 | m_expungeDebounceTimer->setInterval(200); | |
| 46 |
1/1✓ Branch 11 → 12 taken 1050 times.
|
1050 | connect(m_expungeDebounceTimer, &QTimer::timeout, this, [this]() { |
| 47 | ✗ | qCInfo(lcController) << "Debounced expunge: triggering flag sync"; // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; timer state remains measured. | |
| 48 | ✗ | m_imap->executeAfterIdle([this]() { m_imap->fetchFlags(); }); | |
| 49 | ✗ | }); | |
| 50 | 1050 | } | |
| 51 | |||
| 52 | 1802 | void FolderFlagSync::cancelMembershipRead() { | |
| 53 | 1802 | ++m_flagMembershipSequence; | |
| 54 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1802 times.
|
1802 | if (m_flagMembershipCancelled) m_flagMembershipCancelled->store(true); |
| 55 | 1802 | m_flagMembershipCancelled.reset(); | |
| 56 | 1802 | } | |
| 57 | |||
| 58 | 1050 | void FolderFlagSync::abortMembershipRead() { | |
| 59 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1050 times.
|
1050 | if (m_flagMembershipCancelled) m_flagMembershipCancelled->store(true); |
| 60 | 1050 | } | |
| 61 | |||
| 62 |
1/1✓ Branch 3 → 4 taken 1050 times.
|
1050 | void FolderFlagSync::waitForMembershipReads() { m_flagMembershipPool.waitForDone(); } |
| 63 | |||
| 64 | 1043 | void FolderFlagSync::stopPolling() { m_pollingTimer->stop(); } | |
| 65 | |||
| 66 | 759 | void FolderFlagSync::restartPolling() { | |
| 67 | 759 | m_pollingTimer->stop(); | |
| 68 |
2/2✓ Branch 4 → 5 taken 743 times.
✓ Branch 4 → 6 taken 16 times.
|
759 | if (!m_subscribedFolders.isEmpty()) |
| 69 | 743 | m_pollingTimer->start(); | |
| 70 | 759 | } | |
| 71 | |||
| 72 | 1043 | void FolderFlagSync::resetPollingIndex() { m_pollingIndex = 0; } | |
| 73 | |||
| 74 | 588 | void FolderFlagSync::clearChangedSinceFlags() { m_changedSinceFlags.clear(); } | |
| 75 | |||
| 76 | 5 | bool FolderFlagSync::expungeRefreshPending() const { | |
| 77 |
3/4✓ Branch 2 → 3 taken 5 times.
✗ Branch 2 → 6 not taken.
✓ Branch 4 → 5 taken 3 times.
✓ Branch 4 → 6 taken 2 times.
|
5 | return m_expungeDebounceTimer && m_expungeDebounceTimer->isActive(); |
| 78 | } | ||
| 79 | |||
| 80 | 2 | bool FolderFlagSync::pollingActive() const { return m_pollingTimer->isActive(); } | |
| 81 | |||
| 82 | 13 | bool FolderFlagSync::idlePushBelongsToCurrentFolder() const { | |
| 83 | // During a folder switch the controller already names the new folder while | ||
| 84 | // the connection still has the old one selected. A push in that window | ||
| 85 | // belongs to the old mailbox; applying it to the new one fetched the old | ||
| 86 | // folder's complete header range from the new folder's max UID (and wrote | ||
| 87 | // flags into the wrong folder). The new folder's SELECT syncs anyway. | ||
| 88 |
1/2✓ Branch 3 → 4 taken 10 times.
✗ Branch 3 → 10 not taken.
|
10 | return m_folder.activeFolderGen == m_folder.folderGeneration && m_folder.currentFolderId > 0 && |
| 89 |
6/8✓ Branch 2 → 3 taken 10 times.
✓ Branch 2 → 10 taken 3 times.
✓ Branch 5 → 6 taken 10 times.
✗ Branch 5 → 10 not taken.
✓ Branch 8 → 9 taken 10 times.
✗ Branch 8 → 10 not taken.
✓ Branch 12 → 13 taken 10 times.
✓ Branch 12 → 14 taken 3 times.
|
23 | !m_folder.currentFolder.isEmpty() && m_imap->selectedFolder() == m_folder.currentFolder; |
| 90 | } | ||
| 91 | |||
| 92 | 2 | void FolderFlagSync::onIdleNewMessages(int newCount) { | |
| 93 |
2/2✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 17 taken 1 time.
|
2 | if (!idlePushBelongsToCurrentFolder()) { |
| 94 |
6/6✓ Branch 4 → 5 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 15 → 6 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
|
2 | qCInfo(lcController) << "IDLE: ignoring" << newCount |
| 95 |
1/1✓ Branch 11 → 12 taken 1 time.
|
1 | << "new messages pushed during a folder switch"; |
| 96 | 1 | return; | |
| 97 | } | ||
| 98 |
7/7✓ Branch 17 → 18 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 22 → 23 taken 1 time.
✓ Branch 23 → 24 taken 1 time.
✓ Branch 24 → 25 taken 1 time.
✓ Branch 29 → 19 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
|
2 | qCInfo(lcController) << "IDLE:" << newCount << "new messages in" |
| 99 |
1/1✓ Branch 25 → 26 taken 1 time.
|
1 | << m_folder.currentFolder; |
| 100 | |||
| 101 |
1/1✓ Branch 30 → 31 taken 1 time.
|
1 | MailJD::Performance::BackgroundPerformanceTracker::instance() |
| 102 |
1/1✓ Branch 31 → 32 taken 1 time.
|
1 | .idleDeliveryObserved(newCount); |
| 103 | |||
| 104 |
1/1✓ Branch 32 → 33 taken 1 time.
|
1 | qint64 maxUid = m_cache->maxUid(m_folder.currentFolderId); |
| 105 | |||
| 106 | // executeAfterIdle: sends DONE, waits for OK, then fetches. The DONE may | ||
| 107 | // complete after another folder switch; recheck the identity then. | ||
| 108 | 1 | const quint64 generation = m_folder.folderGeneration; | |
| 109 | 1 | const QString folder = m_folder.currentFolder; | |
| 110 |
2/2✓ Branch 35 → 36 taken 1 time.
✓ Branch 36 → 37 taken 1 time.
|
1 | m_imap->executeAfterIdle( |
| 111 | 2 | [this, maxUid, generation, folder]() { | |
| 112 |
3/6✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 7 not taken.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 7 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 1 time.
|
2 | if (generation != m_folder.folderGeneration || folder != m_folder.currentFolder || |
| 113 |
1/2✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 1 time.
|
1 | !idlePushBelongsToCurrentFolder()) return; |
| 114 |
1/2✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 14 not taken.
|
1 | if (m_deps.beginHeaderStream()) m_imap->fetchHeaders(maxUid + 1); |
| 115 | }); | ||
| 116 | |||
| 117 |
1/1✓ Branch 44 → 45 taken 1 time.
|
1 | m_deps.statusMessage( |
| 118 |
3/3✓ Branch 39 → 40 taken 1 time.
✓ Branch 41 → 42 taken 1 time.
✓ Branch 43 → 44 taken 1 time.
|
4 | QString("%1 – %2 new mail(s)!").arg(m_folder.currentFolder).arg(newCount)); |
| 119 | 1 | } | |
| 120 | |||
| 121 | 5 | void FolderFlagSync::onIdleFlagsChanged(qint64 uid, quint32 flags) { | |
| 122 | // This comes during IDLE (no stop needed – it's a push notification) | ||
| 123 |
2/2✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 4 times.
|
5 | if (!idlePushBelongsToCurrentFolder()) return; // old folder's UID space |
| 124 |
8/8✓ Branch 5 → 6 taken 4 times.
✓ Branch 9 → 10 taken 4 times.
✓ Branch 10 → 11 taken 4 times.
✓ Branch 11 → 12 taken 4 times.
✓ Branch 12 → 13 taken 4 times.
✓ Branch 13 → 14 taken 4 times.
✓ Branch 17 → 7 taken 4 times.
✓ Branch 17 → 18 taken 4 times.
|
8 | qCInfo(lcController) << "IDLE: flags changed for UID" << uid << "→" << flags; |
| 125 | auto &backgroundTracker = | ||
| 126 | 4 | MailJD::Performance::BackgroundPerformanceTracker::instance(); | |
| 127 |
3/5✓ Branch 23 → 24 taken 4 times.
✓ Branch 26 → 27 taken 4 times.
✓ Branch 26 → 28 taken 4 times.
✗ Branch 65 → 66 not taken.
✗ Branch 65 → 67 not taken.
|
8 | backgroundTracker.beginBackground( |
| 128 | 4 | {QStringLiteral("mutation_during_idle_refresh")}, -1, 1); | |
| 129 |
1/1✓ Branch 32 → 33 taken 4 times.
|
4 | backgroundTracker.backgroundPhaseStarted(QStringLiteral("idle_refresh")); |
| 130 | |||
| 131 | // T-201: Server confirmed the flag change → remove from pending set | ||
| 132 |
1/3✓ Branch 36 → 37 taken 4 times.
✗ Branch 86 → 87 not taken.
✗ Branch 86 → 88 not taken.
|
4 | m_flagIntents.clearFlag(MailId{m_folder.accountId, m_folder.currentFolderId, uid}); |
| 133 | |||
| 134 | 4 | m_cache->updateFlags(m_folder.currentFolderId, uid, flags); | |
| 135 | 4 | m_model->updateFlags(uid, flags, m_folder.currentFolderId); | |
| 136 |
1/2✓ Branch 40 → 41 taken 4 times.
✗ Branch 40 → 42 not taken.
|
4 | if (m_threadModel) |
| 137 | 4 | m_threadModel->updateFlags(uid, flags, m_folder.currentFolderId); | |
| 138 | // Mail outside the loaded window changes only the cache: its settled | ||
| 139 | // count is the folder total (one indexed point read on this push). | ||
| 140 |
2/2✓ Branch 43 → 44 taken 2 times.
✓ Branch 43 → 46 taken 2 times.
|
4 | if (m_model->rowForUid(uid, m_folder.currentFolderId) < 0) |
| 141 | 2 | m_model->setAuthoritativeUnread(m_cache->unreadCount(m_folder.currentFolderId)); | |
| 142 | 4 | m_deps.unreadCountChanged(m_folder.currentFolder, m_model->unreadCount()); | |
| 143 | 4 | backgroundTracker.addBackgroundWork(1); | |
| 144 |
1/1✓ Branch 52 → 53 taken 4 times.
|
4 | backgroundTracker.backgroundPhaseFinished(QStringLiteral("idle_refresh"), 1); |
| 145 | 4 | backgroundTracker.backgroundFinished(true); | |
| 146 | 8 | } | |
| 147 | |||
| 148 | 4 | void FolderFlagSync::onIdleMessageExpunged(int seqNo) { | |
| 149 |
2/2✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 3 times.
|
4 | if (!idlePushBelongsToCurrentFolder()) return; // old folder's sequence space |
| 150 |
6/6✓ Branch 5 → 6 taken 3 times.
✓ Branch 9 → 10 taken 3 times.
✓ Branch 10 → 11 taken 3 times.
✓ Branch 11 → 12 taken 3 times.
✓ Branch 15 → 7 taken 3 times.
✓ Branch 15 → 16 taken 3 times.
|
6 | qCInfo(lcController) << "IDLE: message expunged, seqNo" << seqNo; |
| 151 | |||
| 152 | // Debounce: coalesce rapid expunge events (e.g. bulk deletes) | ||
| 153 | // into a single fetchFlags call after 200ms of quiet. | ||
| 154 | 3 | m_expungeDebounceTimer->start(); | |
| 155 | } | ||
| 156 | |||
| 157 | 1 | void FolderFlagSync::onIdleFlagsNeedRefetch(int seqNo) { | |
| 158 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1 time.
|
1 | if (!idlePushBelongsToCurrentFolder()) return; // old folder's sequence space |
| 159 |
6/6✓ Branch 5 → 6 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 15 → 7 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
|
2 | qCInfo(lcController) << "IDLE: flag change without UID, seqNo" << seqNo; |
| 160 | // T-065: Stop IDLE, fetch UID+FLAGS for this sequence number, | ||
| 161 | // result flows through onFlagsReceived which restarts IDLE. | ||
| 162 |
1/1✓ Branch 17 → 18 taken 1 time.
|
1 | m_imap->executeAfterIdle([this, seqNo]() { |
| 163 | 1 | m_imap->fetchUidForSeqNo(seqNo); | |
| 164 | 1 | }); | |
| 165 | } | ||
| 166 | |||
| 167 | 17 | void FolderFlagSync::onFlagsReceived( | |
| 168 | const QList<QPair<qint64, quint32>> &uidFlags) { | ||
| 169 | // T-113: Discard if folder changed | ||
| 170 |
2/2✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 20 taken 16 times.
|
17 | if (m_folder.activeFolderGen != m_folder.folderGeneration) { |
| 171 |
1/1✓ Branch 3 → 4 taken 1 time.
|
1 | MailJD::Performance::FolderPerformanceTracker::instance() |
| 172 |
1/1✓ Branch 4 → 5 taken 1 time.
|
1 | .staleGenerationDiscarded(m_folder.activeFolderGen); |
| 173 |
6/6✓ Branch 5 → 6 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 18 → 7 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
|
2 | qCInfo(lcController) << "Discarding stale flags (gen" << m_folder.activeFolderGen |
| 174 |
3/3✓ Branch 12 → 13 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
|
1 | << "vs" << m_folder.folderGeneration << ")"; |
| 175 | 6 | return; | |
| 176 | } | ||
| 177 | |||
| 178 |
3/3✓ Branch 20 → 21 taken 16 times.
✓ Branch 21 → 22 taken 2 times.
✓ Branch 21 → 33 taken 14 times.
|
16 | if (m_deps.pendingHeaderWrites() > 0) { |
| 179 |
1/2✗ Branch 23 → 24 not taken.
✓ Branch 23 → 31 taken 2 times.
|
2 | if (uidFlags.size() > (16LL * 1024 * 1024) / sizeof(QPair<qint64, quint32>)) { |
| 180 | ✗ | m_deps.failHeaderWrite(QStringLiteral("Deferred flag snapshot exceeds its byte budget")); | |
| 181 | ✗ | return; | |
| 182 | } | ||
| 183 | // This signal contains a complete FETCH FLAGS snapshot. Only its newest | ||
| 184 | // version is needed, and it cannot finish a sync before its headers commit. | ||
| 185 |
1/1✓ Branch 31 → 32 taken 2 times.
|
2 | m_deps.deferHeaderFlags(uidFlags); |
| 186 | 2 | return; | |
| 187 | } | ||
| 188 | |||
| 189 |
7/7✓ Branch 33 → 34 taken 14 times.
✓ Branch 37 → 38 taken 14 times.
✓ Branch 38 → 39 taken 14 times.
✓ Branch 40 → 41 taken 14 times.
✓ Branch 41 → 42 taken 14 times.
✓ Branch 45 → 35 taken 14 times.
✓ Branch 45 → 46 taken 14 times.
|
28 | qCInfo(lcController) << "Flag sync:" << uidFlags.size() << "UID/flag pairs"; |
| 190 | 14 | ++m_flagMembershipSequence; | |
| 191 |
1/2✗ Branch 47 → 48 not taken.
✓ Branch 47 → 50 taken 14 times.
|
14 | if (m_flagMembershipCancelled) m_flagMembershipCancelled->store(true); |
| 192 | 14 | m_flagMembershipCancelled.reset(); | |
| 193 | // F-66 (SPRINT-104): canonical membership must not depend on the flat | ||
| 194 | // model being complete. Whenever the model is filtered, windowed or | ||
| 195 | // delegates presentation to indexed rows (an indexed-flags provider is | ||
| 196 | // registered), the exact worker path reconciles cache membership — the | ||
| 197 | // small-folder immediate path would otherwise treat every off-model | ||
| 198 | // cached UID as missing and keep remotely expunged rows forever. | ||
| 199 | ✗ | const bool modelOwnsEveryCachedRow = [this] { | |
| 200 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 14 times.
|
14 | if (m_visibleIndexedFlags) return false; |
| 201 | 14 | int rows = 0; | |
| 202 |
3/3✓ Branch 12 → 13 taken 118 times.
✓ Branch 13 → 6 taken 104 times.
✓ Branch 13 → 14 taken 14 times.
|
118 | for (int r = 0; r < m_model->rowCount(); ++r) { |
| 203 | 104 | const auto *h = m_model->headerAt(r); | |
| 204 |
2/4✓ Branch 7 → 8 taken 104 times.
✗ Branch 7 → 10 not taken.
✓ Branch 8 → 9 taken 104 times.
✗ Branch 8 → 10 not taken.
|
104 | if (h && h->folderId == m_folder.currentFolderId) ++rows; |
| 205 | } | ||
| 206 | 14 | return rows >= m_cache->headerCount(m_folder.currentFolderId); | |
| 207 |
1/1✓ Branch 51 → 52 taken 14 times.
|
14 | }(); |
| 208 | // The loaded folder's authoritative total can select the conservative | ||
| 209 | // worker path immediately. Only an apparently small snapshot needs the | ||
| 210 | // canonical count lookup to detect off-page cached rows. | ||
| 211 | 14 | if (uidFlags.size() > MailHeaderPageSnapshot::MaximumRows || | |
| 212 |
2/4✓ Branch 54 → 55 taken 13 times.
✗ Branch 54 → 57 not taken.
✓ Branch 56 → 57 taken 13 times.
✗ Branch 56 → 60 not taken.
|
26 | (m_folderPresentationEnabled && |
| 213 | 13 | m_model->totalHeaderCount() > MailHeaderPageSnapshot::MaximumRows) || | |
| 214 |
6/7✓ Branch 53 → 54 taken 13 times.
✓ Branch 53 → 60 taken 1 time.
✓ Branch 57 → 58 taken 13 times.
✓ Branch 58 → 59 taken 13 times.
✗ Branch 58 → 60 not taken.
✓ Branch 62 → 63 taken 3 times.
✓ Branch 62 → 67 taken 11 times.
|
40 | m_cache->headerCount(m_folder.currentFolderId) > MailHeaderPageSnapshot::MaximumRows || |
| 215 |
2/2✓ Branch 59 → 60 taken 2 times.
✓ Branch 59 → 61 taken 11 times.
|
13 | !modelOwnsEveryCachedRow) { |
| 216 |
1/1✓ Branch 64 → 65 taken 3 times.
|
3 | reconcileLargeFlagSnapshot(uidFlags); |
| 217 | 3 | return; | |
| 218 | } | ||
| 219 | |||
| 220 | // Build server UID set for expunge detection | ||
| 221 | 11 | QSet<qint64> serverUids; | |
| 222 | 11 | QList<QPair<qint64, quint32>> changed; | |
| 223 | |||
| 224 |
2/2✓ Branch 89 → 70 taken 58 times.
✓ Branch 89 → 90 taken 11 times.
|
69 | for (const auto &[uid, serverFlags] : uidFlags) { |
| 225 |
1/1✓ Branch 73 → 74 taken 58 times.
|
58 | serverUids.insert(uid); |
| 226 | // T-201: Skip UIDs with pending optimistic flag updates | ||
| 227 | 233 | if (m_flagIntents.hasPendingFlag( | |
| 228 |
3/5✓ Branch 75 → 76 taken 58 times.
✓ Branch 77 → 78 taken 1 time.
✓ Branch 77 → 79 taken 57 times.
✗ Branch 209 → 210 not taken.
✗ Branch 209 → 211 not taken.
|
58 | MailId{m_folder.accountId, m_folder.currentFolderId, uid})) |
| 229 | 1 | continue; | |
| 230 |
1/1✓ Branch 79 → 80 taken 57 times.
|
57 | int row = m_model->rowForUid(uid, m_folder.currentFolderId); |
| 231 |
2/2✓ Branch 80 → 81 taken 51 times.
✓ Branch 80 → 87 taken 6 times.
|
57 | if (row >= 0) { |
| 232 |
1/1✓ Branch 81 → 82 taken 51 times.
|
51 | auto *h = m_model->headerAt(row); |
| 233 |
3/4✓ Branch 82 → 83 taken 51 times.
✗ Branch 82 → 87 not taken.
✓ Branch 83 → 84 taken 1 time.
✓ Branch 83 → 87 taken 50 times.
|
51 | if (h && h->flags != serverFlags) { |
| 234 |
1/1✓ Branch 85 → 86 taken 1 time.
|
1 | changed.append({uid, serverFlags}); |
| 235 | } | ||
| 236 | } | ||
| 237 | } | ||
| 238 | |||
| 239 | // Detect expunged UIDs (in model but not on server) | ||
| 240 | 11 | QList<qint64> expunged; | |
| 241 |
3/3✓ Branch 102 → 103 taken 64 times.
✓ Branch 103 → 91 taken 53 times.
✓ Branch 103 → 104 taken 11 times.
|
64 | for (int r = 0; r < m_model->rowCount(); ++r) { |
| 242 |
1/1✓ Branch 91 → 92 taken 53 times.
|
53 | auto *h = m_model->headerAt(r); |
| 243 |
6/8✓ Branch 92 → 93 taken 53 times.
✗ Branch 92 → 97 not taken.
✓ Branch 93 → 94 taken 53 times.
✗ Branch 93 → 97 not taken.
✓ Branch 95 → 96 taken 1 time.
✓ Branch 95 → 97 taken 52 times.
✓ Branch 98 → 99 taken 1 time.
✓ Branch 98 → 100 taken 52 times.
|
53 | if (h && h->folderId == m_folder.currentFolderId && !serverUids.contains(h->uid)) { |
| 244 |
1/1✓ Branch 99 → 100 taken 1 time.
|
1 | expunged.append(h->uid); |
| 245 | } | ||
| 246 | } | ||
| 247 | |||
| 248 | // Apply changes | ||
| 249 |
2/2✓ Branch 105 → 106 taken 1 time.
✓ Branch 105 → 133 taken 10 times.
|
11 | if (!changed.isEmpty()) { |
| 250 |
1/1✓ Branch 106 → 107 taken 1 time.
|
1 | m_cache->batchUpdateFlags(m_folder.currentFolderId, changed); |
| 251 |
4/4✓ Branch 107 → 108 taken 1 time.
✓ Branch 108 → 109 taken 1 time.
✓ Branch 118 → 110 taken 1 time.
✓ Branch 118 → 119 taken 1 time.
|
2 | for (const auto &[uid, flags] : changed) { |
| 252 |
1/1✓ Branch 113 → 114 taken 1 time.
|
1 | m_model->updateFlags(uid, flags, m_folder.currentFolderId); |
| 253 |
1/2✓ Branch 114 → 115 taken 1 time.
✗ Branch 114 → 116 not taken.
|
1 | if (m_threadModel) |
| 254 |
1/1✓ Branch 115 → 116 taken 1 time.
|
1 | m_threadModel->updateFlags(uid, flags, m_folder.currentFolderId); |
| 255 | } | ||
| 256 |
7/7✓ Branch 119 → 120 taken 1 time.
✓ Branch 123 → 124 taken 1 time.
✓ Branch 124 → 125 taken 1 time.
✓ Branch 126 → 127 taken 1 time.
✓ Branch 127 → 128 taken 1 time.
✓ Branch 131 → 121 taken 1 time.
✓ Branch 131 → 132 taken 1 time.
|
2 | qCInfo(lcController) << "Flag sync: updated" << changed.size() << "flags"; |
| 257 | } | ||
| 258 | |||
| 259 |
4/4✓ Branch 133 → 134 taken 11 times.
✓ Branch 134 → 135 taken 11 times.
✓ Branch 152 → 136 taken 1 time.
✓ Branch 152 → 153 taken 11 times.
|
12 | for (qint64 uid : expunged) { |
| 260 |
1/1✓ Branch 137 → 138 taken 1 time.
|
1 | m_cache->removeHeader(m_folder.currentFolderId, uid); |
| 261 |
1/1✓ Branch 138 → 139 taken 1 time.
|
1 | m_model->removeByUid(uid, m_folder.currentFolderId); |
| 262 |
6/6✓ Branch 139 → 140 taken 1 time.
✓ Branch 143 → 144 taken 1 time.
✓ Branch 144 → 145 taken 1 time.
✓ Branch 145 → 146 taken 1 time.
✓ Branch 149 → 141 taken 1 time.
✓ Branch 149 → 150 taken 1 time.
|
2 | qCInfo(lcController) << "Flag sync: removed expunged UID" << uid; |
| 263 | } | ||
| 264 | |||
| 265 | // Small folders retain the existing immediate path. The full page owns | ||
| 266 | // their cached rows, and synchronous controller tests keep their contract. | ||
| 267 | 11 | QSet<qint64> modelUids; | |
| 268 |
3/3✓ Branch 162 → 163 taken 63 times.
✓ Branch 163 → 155 taken 52 times.
✓ Branch 163 → 164 taken 11 times.
|
63 | for (int r = 0; r < m_model->rowCount(); ++r) { |
| 269 |
1/1✓ Branch 155 → 156 taken 52 times.
|
52 | const auto *h = m_model->headerAt(r); |
| 270 |
3/5✓ Branch 156 → 157 taken 52 times.
✗ Branch 156 → 160 not taken.
✓ Branch 157 → 158 taken 52 times.
✗ Branch 157 → 160 not taken.
✓ Branch 158 → 159 taken 52 times.
|
52 | if (h && h->folderId == m_folder.currentFolderId) modelUids.insert(h->uid); |
| 271 | } | ||
| 272 | 11 | QList<qint64> missingUids; | |
| 273 |
4/4✓ Branch 164 → 165 taken 11 times.
✓ Branch 165 → 166 taken 11 times.
✓ Branch 173 → 167 taken 58 times.
✓ Branch 173 → 174 taken 11 times.
|
69 | for (qint64 uid : serverUids) |
| 274 |
3/3✓ Branch 169 → 170 taken 6 times.
✓ Branch 169 → 171 taken 52 times.
✓ Branch 170 → 171 taken 6 times.
|
58 | if (!modelUids.contains(uid)) missingUids.append(uid); |
| 275 |
1/1✓ Branch 176 → 177 taken 11 times.
|
11 | m_deps.finishFlagSync(std::move(missingUids), false); |
| 276 | 11 | } | |
| 277 | |||
| 278 | 2 | void FolderFlagSync::onChangedFlagsReceived( | |
| 279 | const QList<QPair<qint64, quint32>> &uidFlags) { | ||
| 280 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 2 times.
|
2 | if (m_folder.activeFolderGen != m_folder.folderGeneration) return; |
| 281 | // Held until the pipelined UID membership arrives. | ||
| 282 | 2 | m_changedSinceFlags = uidFlags; | |
| 283 | } | ||
| 284 | |||
| 285 | 2 | void FolderFlagSync::onUidMembershipReceived(const QList<qint64> &uids) { | |
| 286 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 5 taken 2 times.
|
2 | if (m_folder.activeFolderGen != m_folder.folderGeneration) { |
| 287 | ✗ | m_changedSinceFlags.clear(); | |
| 288 | ✗ | return; | |
| 289 | } | ||
| 290 |
6/6✓ Branch 5 → 6 taken 2 times.
✓ Branch 9 → 10 taken 2 times.
✓ Branch 10 → 11 taken 2 times.
✓ Branch 12 → 13 taken 2 times.
✓ Branch 20 → 7 taken 2 times.
✓ Branch 20 → 21 taken 2 times.
|
4 | qCInfo(lcController) << "CONDSTORE sync:" << m_changedSinceFlags.size() |
| 291 |
3/3✓ Branch 13 → 14 taken 2 times.
✓ Branch 15 → 16 taken 2 times.
✓ Branch 16 → 17 taken 2 times.
|
2 | << "changed of" << uids.size() << "UIDs"; |
| 292 | 2 | ++m_flagMembershipSequence; | |
| 293 |
1/2✗ Branch 22 → 23 not taken.
✓ Branch 22 → 25 taken 2 times.
|
2 | if (m_flagMembershipCancelled) m_flagMembershipCancelled->store(true); |
| 294 | 2 | m_flagMembershipCancelled.reset(); | |
| 295 | // Always the worker path: a partial flag list can never be compared | ||
| 296 | // against the loaded page as if it were complete. | ||
| 297 |
1/1✓ Branch 29 → 30 taken 2 times.
|
2 | reconcileLargeFlagSnapshot(std::exchange(m_changedSinceFlags, {}), uids); |
| 298 | } | ||
| 299 | |||
| 300 | 5 | void FolderFlagSync::reconcileLargeFlagSnapshot( | |
| 301 | const QList<QPair<qint64, quint32>> &uidFlags, | ||
| 302 | std::optional<QList<qint64>> membership) { | ||
| 303 | // Only loaded rows need immediate visual updates. Snapshot their flags on | ||
| 304 | // the GUI thread; scan the potentially million-entry server set and cached | ||
| 305 | // membership on the read-only worker. | ||
| 306 | 5 | QHash<qint64, quint32> visibleFlags; | |
| 307 |
3/3✓ Branch 9 → 10 taken 56 times.
✓ Branch 10 → 3 taken 51 times.
✓ Branch 10 → 11 taken 5 times.
|
56 | for (int row = 0; row < m_model->rowCount(); ++row) { |
| 308 |
1/1✓ Branch 3 → 4 taken 51 times.
|
51 | const auto *header = m_model->headerAt(row); |
| 309 |
2/4✓ Branch 4 → 5 taken 51 times.
✗ Branch 4 → 7 not taken.
✓ Branch 5 → 6 taken 51 times.
✗ Branch 5 → 7 not taken.
|
51 | if (header && header->folderId == m_folder.currentFolderId) |
| 310 |
1/1✓ Branch 6 → 7 taken 51 times.
|
51 | visibleFlags.insert(header->uid, header->flags); |
| 311 | } | ||
| 312 |
1/2✗ Branch 12 → 13 not taken.
✓ Branch 12 → 24 taken 5 times.
|
5 | if (m_visibleIndexedFlags) { |
| 313 | ✗ | const auto indexed = m_visibleIndexedFlags(m_folder.currentFolderId); | |
| 314 | ✗ | for (auto it = indexed.cbegin(); it != indexed.cend(); ++it) | |
| 315 | ✗ | visibleFlags.insert(it.key(), it.value()); | |
| 316 | ✗ | } | |
| 317 | 5 | QSet<qint64> pendingFlags; | |
| 318 |
1/2✗ Branch 39 → 28 not taken.
✓ Branch 39 → 40 taken 5 times.
|
5 | for (const auto &id : m_flagIntents.pendingFlags()) |
| 319 | ✗ | if (id.accountId == m_folder.accountId && id.folderId == m_folder.currentFolderId) | |
| 320 | ✗ | pendingFlags.insert(id.uid); | |
| 321 | |||
| 322 | 5 | const auto sequence = m_flagMembershipSequence; | |
| 323 | 5 | const auto generation = m_folder.folderGeneration; | |
| 324 | 5 | const auto folderId = m_folder.currentFolderId; | |
| 325 | 5 | const auto folder = m_folder.currentFolder; | |
| 326 | 5 | const auto account = m_folder.accountId; | |
| 327 | // compareFlagSnapshot validates this selected epoch inside its read-only | ||
| 328 | // transaction; the GUI-side post-result check below still detects a later | ||
| 329 | // cache identity change before applying any delta. | ||
| 330 | 5 | const auto epoch = m_folder.activeFolderUidValidity; | |
| 331 | 5 | const auto databasePath = m_cache->databasePath(); | |
| 332 |
1/1✓ Branch 43 → 44 taken 5 times.
|
5 | auto cancelled = std::make_shared<std::atomic_bool>(false); |
| 333 | 5 | m_flagMembershipCancelled = cancelled; | |
| 334 |
3/6✓ Branch 45 → 46 taken 5 times.
✓ Branch 46 → 47 taken 5 times.
✗ Branch 47 → 48 not taken.
✓ Branch 47 → 49 taken 5 times.
✗ Branch 85 → 86 not taken.
✗ Branch 85 → 87 not taken.
|
5 | auto *watcher = new QFutureWatcher<MailCache::FlagSnapshotDiff>(this); |
| 335 | 5 | connect(watcher, &QFutureWatcher<MailCache::FlagSnapshotDiff>::finished, | |
| 336 |
1/5✓ Branch 52 → 53 taken 5 times.
✗ Branch 90 → 91 not taken.
✗ Branch 90 → 92 not taken.
✗ Branch 93 → 94 not taken.
✗ Branch 93 → 95 not taken.
|
10 | this, [this, watcher, sequence, generation, folderId, folder, account, |
| 337 | epoch, visibleFlags] { | ||
| 338 |
1/1✓ Branch 2 → 3 taken 5 times.
|
5 | auto result = watcher->result(); |
| 339 |
1/1✓ Branch 3 → 4 taken 5 times.
|
5 | watcher->deleteLater(); |
| 340 |
1/2✓ Branch 5 → 6 taken 5 times.
✗ Branch 5 → 12 not taken.
|
5 | if (sequence != m_flagMembershipSequence || generation != m_folder.folderGeneration || |
| 341 |
2/4✓ Branch 6 → 7 taken 5 times.
✗ Branch 6 → 12 not taken.
✓ Branch 8 → 9 taken 5 times.
✗ Branch 8 → 12 not taken.
|
5 | folderId != m_folder.currentFolderId || folder != m_folder.currentFolder || |
| 342 |
4/8✓ Branch 4 → 5 taken 5 times.
✗ Branch 4 → 12 not taken.
✓ Branch 10 → 11 taken 5 times.
✗ Branch 10 → 12 not taken.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 5 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 5 times.
|
10 | account != m_folder.accountId || result.cancelled) |
| 343 | ✗ | return; | |
| 344 | 5 | m_flagMembershipCancelled.reset(); | |
| 345 |
4/7✓ Branch 17 → 18 taken 5 times.
✗ Branch 17 → 20 not taken.
✓ Branch 18 → 19 taken 5 times.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 5 times.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 25 taken 5 times.
|
5 | if (result.stale || epoch != m_cache->uidValidity(folderId)) { |
| 346 | ✗ | m_deps.resyncCurrentFolder(); | |
| 347 | ✗ | return; | |
| 348 | } | ||
| 349 |
1/2✗ Branch 26 → 27 not taken.
✓ Branch 26 → 40 taken 5 times.
|
5 | if (!result.error.isEmpty()) |
| 350 | ✗ | qCWarning(lcController) << "Exact cached UID membership unavailable:" | |
| 351 | ✗ | << result.error << "- using conservative backfill"; | |
| 352 | |||
| 353 | 5 | const auto currentIndexed = m_visibleIndexedFlags | |
| 354 |
1/3✗ Branch 41 → 42 not taken.
✓ Branch 41 → 43 taken 5 times.
✗ Branch 42 → 44 not taken.
|
5 | ? m_visibleIndexedFlags(folderId) : QHash<qint64, quint32>{}; |
| 355 | 5 | QList<QPair<qint64, quint32>> changed; | |
| 356 |
4/4✓ Branch 44 → 45 taken 5 times.
✓ Branch 45 → 46 taken 5 times.
✓ Branch 87 → 47 taken 1 time.
✓ Branch 87 → 88 taken 5 times.
|
6 | for (const auto &[uid, flags] : result.visibleChanged) { |
| 357 |
2/5✓ Branch 51 → 52 taken 1 time.
✗ Branch 53 → 54 not taken.
✓ Branch 53 → 55 taken 1 time.
✗ Branch 222 → 223 not taken.
✗ Branch 222 → 224 not taken.
|
1 | if (m_flagIntents.hasPendingFlag(MailId{account, folderId, uid})) continue; |
| 358 |
1/1✓ Branch 55 → 56 taken 1 time.
|
1 | const int row = m_model->rowForUid(uid, folderId); |
| 359 |
1/1✓ Branch 56 → 57 taken 1 time.
|
1 | const auto *header = m_model->headerAt(row); |
| 360 | 1 | const auto expected = visibleFlags.constFind(uid); | |
| 361 | 1 | const auto indexed = currentIndexed.constFind(uid); | |
| 362 |
2/6✗ Branch 62 → 63 not taken.
✓ Branch 62 → 66 taken 1 time.
✗ Branch 65 → 66 not taken.
✗ Branch 65 → 78 not taken.
✓ Branch 66 → 67 taken 1 time.
✗ Branch 66 → 69 not taken.
|
2 | if (expected != visibleFlags.cend() && (header || indexed != currentIndexed.cend()) && |
| 363 |
2/4✓ Branch 68 → 69 taken 1 time.
✗ Branch 68 → 78 not taken.
✗ Branch 71 → 72 not taken.
✓ Branch 71 → 75 taken 1 time.
|
2 | (!header || header->flags == expected.value()) && |
| 364 |
2/6✓ Branch 61 → 62 taken 1 time.
✗ Branch 61 → 78 not taken.
✗ Branch 74 → 75 not taken.
✗ Branch 74 → 78 not taken.
✓ Branch 79 → 80 taken 1 time.
✗ Branch 79 → 83 not taken.
|
3 | (indexed == currentIndexed.cend() || indexed.value() == expected.value()) && |
| 365 |
1/2✓ Branch 76 → 77 taken 1 time.
✗ Branch 76 → 78 not taken.
|
1 | expected.value() != flags) |
| 366 |
1/1✓ Branch 81 → 82 taken 1 time.
|
1 | changed.append({uid, flags}); |
| 367 | } | ||
| 368 |
2/2✓ Branch 89 → 90 taken 1 time.
✓ Branch 89 → 117 taken 4 times.
|
5 | if (!changed.isEmpty()) { |
| 369 |
1/1✓ Branch 90 → 91 taken 1 time.
|
1 | m_cache->batchUpdateFlags(folderId, changed); |
| 370 |
4/4✓ Branch 91 → 92 taken 1 time.
✓ Branch 92 → 93 taken 1 time.
✓ Branch 102 → 94 taken 1 time.
✓ Branch 102 → 103 taken 1 time.
|
2 | for (const auto &[uid, flags] : changed) { |
| 371 |
1/1✓ Branch 97 → 98 taken 1 time.
|
1 | m_model->updateFlags(uid, flags, folderId); |
| 372 |
2/3✓ Branch 98 → 99 taken 1 time.
✗ Branch 98 → 100 not taken.
✓ Branch 99 → 100 taken 1 time.
|
1 | if (m_threadModel) m_threadModel->updateFlags(uid, flags, folderId); |
| 373 | } | ||
| 374 |
7/7✓ Branch 103 → 104 taken 1 time.
✓ Branch 107 → 108 taken 1 time.
✓ Branch 108 → 109 taken 1 time.
✓ Branch 110 → 111 taken 1 time.
✓ Branch 111 → 112 taken 1 time.
✓ Branch 115 → 105 taken 1 time.
✓ Branch 115 → 116 taken 1 time.
|
2 | qCInfo(lcController) << "Flag sync: updated" << changed.size() << "visible flags"; |
| 375 | } | ||
| 376 |
3/4✓ Branch 117 → 118 taken 5 times.
✓ Branch 118 → 119 taken 5 times.
✗ Branch 145 → 120 not taken.
✓ Branch 145 → 146 taken 5 times.
|
5 | for (qint64 uid : result.visibleExpunged) { |
| 377 | ✗ | if (m_model->rowForUid(uid, folderId) < 0 && !currentIndexed.contains(uid)) continue; | |
| 378 | ✗ | m_cache->removeHeader(folderId, uid); | |
| 379 | ✗ | m_model->removeByUid(uid, folderId); | |
| 380 | ✗ | qCInfo(lcController) << "Flag sync: removed expunged UID" << uid; | |
| 381 | } | ||
| 382 | // Cached mail outside the loaded rows (read, starred or deleted on | ||
| 383 | // another client). An intent that became pending while the worker ran | ||
| 384 | // keeps its local state; the cache signals update any indexed rows. | ||
| 385 | 5 | QList<QPair<qint64, quint32>> offPageChanged; | |
| 386 |
4/4✓ Branch 146 → 147 taken 5 times.
✓ Branch 147 → 148 taken 5 times.
✓ Branch 161 → 149 taken 5 times.
✓ Branch 161 → 162 taken 5 times.
|
10 | for (const auto &[uid, flags] : result.offPageChanged) |
| 387 |
2/5✓ Branch 153 → 154 taken 5 times.
✓ Branch 155 → 156 taken 5 times.
✗ Branch 155 → 159 not taken.
✗ Branch 243 → 244 not taken.
✗ Branch 243 → 245 not taken.
|
5 | if (!m_flagIntents.hasPendingFlag(MailId{account, folderId, uid})) |
| 388 |
1/1✓ Branch 157 → 158 taken 5 times.
|
5 | offPageChanged.append({uid, flags}); |
| 389 |
2/2✓ Branch 163 → 164 taken 3 times.
✓ Branch 163 → 179 taken 2 times.
|
5 | if (!offPageChanged.isEmpty()) { |
| 390 |
1/1✓ Branch 164 → 165 taken 3 times.
|
3 | m_cache->batchUpdateFlags(folderId, offPageChanged); |
| 391 |
7/7✓ Branch 165 → 166 taken 3 times.
✓ Branch 169 → 170 taken 3 times.
✓ Branch 170 → 171 taken 3 times.
✓ Branch 172 → 173 taken 3 times.
✓ Branch 173 → 174 taken 3 times.
✓ Branch 177 → 167 taken 3 times.
✓ Branch 177 → 178 taken 3 times.
|
6 | qCInfo(lcController) << "Flag sync: updated" << offPageChanged.size() << "off-page flags"; |
| 392 | } | ||
| 393 |
4/4✓ Branch 180 → 181 taken 3 times.
✓ Branch 180 → 184 taken 2 times.
✓ Branch 185 → 186 taken 3 times.
✓ Branch 185 → 200 taken 2 times.
|
8 | if (!result.offPageExpunged.isEmpty() && |
| 394 |
2/3✓ Branch 181 → 182 taken 3 times.
✓ Branch 182 → 183 taken 3 times.
✗ Branch 182 → 184 not taken.
|
3 | m_cache->removeHeaders(folderId, result.offPageExpunged)) |
| 395 |
6/6✓ Branch 186 → 187 taken 3 times.
✓ Branch 190 → 191 taken 3 times.
✓ Branch 191 → 192 taken 3 times.
✓ Branch 193 → 194 taken 3 times.
✓ Branch 198 → 188 taken 3 times.
✓ Branch 198 → 199 taken 3 times.
|
6 | qCInfo(lcController) << "Flag sync: removed" << result.offPageExpunged.size() |
| 396 |
1/1✓ Branch 194 → 195 taken 3 times.
|
3 | << "off-page expunged UIDs"; |
| 397 |
1/1✓ Branch 203 → 204 taken 5 times.
|
5 | m_deps.finishFlagSync(std::move(result.missingUids), result.error.isEmpty()); |
| 398 |
1/2✓ Branch 210 → 211 taken 5 times.
✗ Branch 210 → 213 not taken.
|
5 | }); |
| 399 |
1/1✓ Branch 67 → 68 taken 5 times.
|
5 | watcher->setFuture(QtConcurrent::run(&m_flagMembershipPool, |
| 400 |
1/15✓ Branch 66 → 67 taken 5 times.
✗ Branch 102 → 103 not taken.
✗ Branch 102 → 104 not taken.
✗ Branch 105 → 106 not taken.
✗ Branch 105 → 107 not taken.
✗ Branch 108 → 109 not taken.
✗ Branch 108 → 110 not taken.
✗ Branch 111 → 112 not taken.
✗ Branch 111 → 113 not taken.
✗ Branch 114 → 115 not taken.
✗ Branch 114 → 116 not taken.
✗ Branch 117 → 118 not taken.
✗ Branch 117 → 119 not taken.
✗ Branch 120 → 121 not taken.
✗ Branch 120 → 122 not taken.
|
25 | [databasePath, account, folder, folderId, epoch, uidFlags, |
| 401 | 10 | visibleFlags = std::move(visibleFlags), pendingFlags = std::move(pendingFlags), | |
| 402 | 5 | membership = std::move(membership), cancelled]() mutable { | |
| 403 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 8 taken 5 times.
|
5 | if (cancelled->load()) { |
| 404 | ✗ | MailCache::FlagSnapshotDiff result; | |
| 405 | ✗ | result.cancelled = true; | |
| 406 | ✗ | return result; | |
| 407 | ✗ | } | |
| 408 |
1/1✓ Branch 8 → 9 taken 5 times.
|
5 | MailCache reader; |
| 409 |
1/1✓ Branch 9 → 10 taken 5 times.
|
5 | const bool opened = reader.openReadOnly(databasePath); |
| 410 | 5 | auto result = reader.snapshots().compareFlagSnapshot(account, folder, folderId, epoch, | |
| 411 | 5 | uidFlags, visibleFlags, pendingFlags, *cancelled, | |
| 412 |
3/3✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 14 taken 3 times.
✓ Branch 16 → 17 taken 5 times.
|
10 | membership ? &*membership : nullptr); |
| 413 |
3/12✗ Branch 17 → 18 not taken.
✓ Branch 17 → 22 taken 5 times.
✗ Branch 18 → 19 not taken.
✗ Branch 20 → 21 not taken.
✗ Branch 20 → 22 not taken.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 5 times.
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 30 taken 5 times.
✗ Branch 26 → 27 not taken.
✗ Branch 36 → 37 not taken.
✗ Branch 36 → 38 not taken.
|
5 | if (!opened && !reader.lastError().isEmpty()) result.error = reader.lastError(); |
| 414 | 5 | return result; | |
| 415 | 5 | })); | |
| 416 | 5 | } | |
| 417 | |||
| 418 | 5 | void FolderFlagSync::pollFolders() { | |
| 419 |
2/2✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 4 times.
|
5 | if (m_subscribedFolders.isEmpty()) |
| 420 | 1 | return; | |
| 421 | |||
| 422 |
6/6✓ Branch 5 → 6 taken 4 times.
✓ Branch 9 → 10 taken 4 times.
✓ Branch 10 → 11 taken 4 times.
✓ Branch 12 → 13 taken 4 times.
✓ Branch 17 → 7 taken 4 times.
✓ Branch 17 → 18 taken 4 times.
|
8 | qCInfo(lcController) << "Polling" << m_subscribedFolders.size() |
| 423 |
1/1✓ Branch 13 → 14 taken 4 times.
|
4 | << "subscribed folders for unread counts"; |
| 424 | |||
| 425 | 4 | m_pollingIndex = 0; | |
| 426 | |||
| 427 | // T-206: Use search connection for polling to avoid interrupting IDLE | ||
| 428 | 4 | m_deps.ensureSearchConnection(); | |
| 429 |
5/6✓ Branch 19 → 20 taken 4 times.
✗ Branch 19 → 25 not taken.
✓ Branch 21 → 22 taken 3 times.
✓ Branch 21 → 24 taken 1 time.
✓ Branch 26 → 27 taken 1 time.
✓ Branch 26 → 38 taken 3 times.
|
11 | if (m_searchImap && |
| 430 |
1/2✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 3 times.
|
7 | (m_searchImap->state() == ImapService::State::Authenticated || |
| 431 | 3 | m_searchImap->state() == ImapService::State::Selected)) { | |
| 432 | 2 | qCInfo(lcController) << "T-206: Polling via search connection (IDLE preserved)"; // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; connection selection remains measured. | |
| 433 | 1 | pollNextFolder(); | |
| 434 | } else { | ||
| 435 | // Fallback: pause IDLE → run all STATUS commands → re-IDLE | ||
| 436 |
1/1✓ Branch 39 → 40 taken 3 times.
|
6 | m_imap->executeAfterIdle([this]() { pollNextFolder(); }); |
| 437 | } | ||
| 438 | } | ||
| 439 | |||
| 440 | 5 | void FolderFlagSync::triggerPollNow() { | |
| 441 | 5 | pollFolders(); | |
| 442 | 5 | } | |
| 443 | |||
| 444 | 22 | void FolderFlagSync::pollNextFolder() { | |
| 445 | // Skip current folder (already live via IDLE) | ||
| 446 |
6/6✓ Branch 5 → 6 taken 9 times.
✓ Branch 5 → 10 taken 16 times.
✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 10 taken 6 times.
✓ Branch 11 → 3 taken 3 times.
✓ Branch 11 → 12 taken 22 times.
|
34 | while (m_pollingIndex < m_subscribedFolders.size() && |
| 447 | 9 | m_subscribedFolders.at(m_pollingIndex) == m_folder.currentFolder) { | |
| 448 | 3 | m_pollingIndex++; | |
| 449 | } | ||
| 450 | |||
| 451 |
2/2✓ Branch 13 → 14 taken 16 times.
✓ Branch 13 → 28 taken 6 times.
|
22 | if (m_pollingIndex >= m_subscribedFolders.size()) { |
| 452 | // All folders polled → restart IDLE (only needed if using main connection) | ||
| 453 |
5/5✓ Branch 14 → 15 taken 16 times.
✓ Branch 18 → 19 taken 16 times.
✓ Branch 19 → 20 taken 16 times.
✓ Branch 23 → 16 taken 16 times.
✓ Branch 23 → 24 taken 16 times.
|
32 | qCInfo(lcController) << "Polling complete"; |
| 454 |
2/2✓ Branch 25 → 26 taken 12 times.
✓ Branch 25 → 27 taken 4 times.
|
16 | if (!m_imap->isIdling()) { |
| 455 | 12 | startIdleIfPossible(); | |
| 456 | } | ||
| 457 | 16 | return; | |
| 458 | } | ||
| 459 | |||
| 460 | // T-206: Use search connection for STATUS when available | ||
| 461 |
6/6✓ Branch 28 → 29 taken 5 times.
✓ Branch 28 → 34 taken 1 time.
✓ Branch 30 → 31 taken 4 times.
✓ Branch 30 → 33 taken 1 time.
✓ Branch 35 → 36 taken 2 times.
✓ Branch 35 → 38 taken 4 times.
|
15 | if (m_searchImap && |
| 462 |
2/2✓ Branch 32 → 33 taken 1 time.
✓ Branch 32 → 34 taken 3 times.
|
9 | (m_searchImap->state() == ImapService::State::Authenticated || |
| 463 | 4 | m_searchImap->state() == ImapService::State::Selected)) { | |
| 464 | 2 | m_searchImap->statusFolder(m_subscribedFolders.at(m_pollingIndex)); | |
| 465 | } else { | ||
| 466 | 4 | m_imap->statusFolder(m_subscribedFolders.at(m_pollingIndex)); | |
| 467 | } | ||
| 468 | } | ||
| 469 | |||
| 470 | 27 | void FolderFlagSync::onFolderStatusReceived(const StatusResult &result) { | |
| 471 |
6/6✓ Branch 2 → 3 taken 27 times.
✓ Branch 6 → 7 taken 27 times.
✓ Branch 7 → 8 taken 27 times.
✓ Branch 8 → 9 taken 27 times.
✓ Branch 16 → 4 taken 27 times.
✓ Branch 16 → 17 taken 27 times.
|
54 | qCInfo(lcController) << "STATUS" << result.folderPath |
| 472 |
2/2✓ Branch 9 → 10 taken 27 times.
✓ Branch 10 → 11 taken 27 times.
|
27 | << "messages:" << result.messages |
| 473 |
2/2✓ Branch 11 → 12 taken 27 times.
✓ Branch 12 → 13 taken 27 times.
|
27 | << "unseen:" << result.unseen; |
| 474 | |||
| 475 | // A partial STATUS without UNSEEN never publishes a badge and needs no | ||
| 476 | // folder lookup. For the selected folder, reuse the ID admitted at SELECT. | ||
| 477 | // Both responses are common under NOTIFY and otherwise hit GUI SQLite. | ||
| 478 |
2/2✓ Branch 17 → 18 taken 17 times.
✓ Branch 17 → 25 taken 10 times.
|
44 | const qint64 fid = !result.hasUnseen ? -1 |
| 479 |
1/2✓ Branch 20 → 21 taken 13 times.
✗ Branch 20 → 22 not taken.
|
30 | : result.folderPath == m_folder.currentFolder && m_folder.currentFolderId > 0 |
| 480 |
3/3✓ Branch 19 → 20 taken 13 times.
✓ Branch 19 → 22 taken 4 times.
✓ Branch 22 → 23 taken 4 times.
|
30 | ? m_folder.currentFolderId : m_cache->ensureFolder(m_folder.accountId, result.folderPath); |
| 481 | 27 | bool publishCount = result.hasUnseen; | |
| 482 |
4/4✓ Branch 26 → 27 taken 10 times.
✓ Branch 26 → 29 taken 17 times.
✓ Branch 27 → 28 taken 8 times.
✓ Branch 27 → 29 taken 2 times.
|
27 | bool refresh = !result.hasUnseen && result.requestStartedNs == 0; |
| 483 | 27 | auto fence = m_flagIntents.readbackFence(fid); | |
| 484 |
7/7✓ Branch 32 → 33 taken 6 times.
✓ Branch 32 → 37 taken 21 times.
✓ Branch 34 → 35 taken 6 times.
✓ Branch 35 → 36 taken 1 time.
✓ Branch 35 → 37 taken 5 times.
✓ Branch 38 → 39 taken 1 time.
✓ Branch 38 → 41 taken 26 times.
|
27 | if (fence && fence->uidValidity != m_cache->uidValidity(fid)) { |
| 485 |
1/1✓ Branch 39 → 40 taken 1 time.
|
1 | m_flagIntents.clearReadbackFence(fid); |
| 486 | 1 | fence.reset(); | |
| 487 | } | ||
| 488 |
6/6✓ Branch 42 → 43 taken 5 times.
✓ Branch 42 → 46 taken 22 times.
✓ Branch 44 → 45 taken 4 times.
✓ Branch 44 → 46 taken 1 time.
✓ Branch 47 → 48 taken 4 times.
✓ Branch 47 → 55 taken 23 times.
|
27 | if (fence && result.requestStartedNs <= fence->completedNs) { |
| 489 | 4 | publishCount = false; | |
| 490 |
2/4✓ Branch 48 → 49 taken 4 times.
✗ Branch 48 → 52 not taken.
✓ Branch 49 → 50 taken 4 times.
✗ Branch 49 → 53 not taken.
|
8 | refresh = refresh || (result.hasUnseen && |
| 491 |
3/3✓ Branch 50 → 51 taken 4 times.
✓ Branch 51 → 52 taken 3 times.
✓ Branch 51 → 53 taken 1 time.
|
4 | m_flagIntents.lastPolledUnread(result.folderPath, -1) != result.unseen); |
| 492 | } | ||
| 493 | // Pending Seen intent needs complete membership evidence before an aggregate | ||
| 494 | // can replace the optimistic count; an omitted MESSAGES field is unknown. | ||
| 495 | 27 | int unread = result.unseen; | |
| 496 |
5/6✓ Branch 55 → 56 taken 13 times.
✓ Branch 55 → 61 taken 14 times.
✓ Branch 56 → 57 taken 13 times.
✗ Branch 56 → 61 not taken.
✓ Branch 62 → 63 taken 3 times.
✓ Branch 62 → 73 taken 24 times.
|
40 | if (publishCount && fid >= 0 && |
| 497 |
3/3✓ Branch 58 → 59 taken 13 times.
✓ Branch 59 → 60 taken 3 times.
✓ Branch 59 → 61 taken 10 times.
|
13 | m_cache->journal().hasPendingSeenMutation(m_folder.accountId, fid)) { |
| 498 |
2/2✓ Branch 63 → 64 taken 2 times.
✓ Branch 63 → 69 taken 1 time.
|
3 | if (!result.hasMessages) { |
| 499 | 2 | publishCount = false; | |
| 500 |
3/4✓ Branch 64 → 65 taken 2 times.
✗ Branch 64 → 66 not taken.
✓ Branch 65 → 66 taken 1 time.
✓ Branch 65 → 67 taken 1 time.
|
2 | refresh = refresh || result.requestStartedNs == 0; |
| 501 |
2/3✓ Branch 69 → 70 taken 1 time.
✓ Branch 70 → 71 taken 1 time.
✗ Branch 70 → 73 not taken.
|
1 | } else if (result.messages == m_cache->headerCount(fid)) { |
| 502 |
1/1✓ Branch 71 → 72 taken 1 time.
|
1 | unread = m_cache->unreadCount(fid); |
| 503 | } | ||
| 504 | } | ||
| 505 |
2/2✓ Branch 73 → 74 taken 12 times.
✓ Branch 73 → 96 taken 15 times.
|
27 | if (refresh) { |
| 506 | 12 | ImapService *service = m_imap; | |
| 507 |
6/6✓ Branch 74 → 75 taken 3 times.
✓ Branch 74 → 80 taken 9 times.
✓ Branch 76 → 77 taken 2 times.
✓ Branch 76 → 79 taken 1 time.
✓ Branch 81 → 82 taken 2 times.
✓ Branch 81 → 83 taken 10 times.
|
17 | if (m_searchImap && |
| 508 |
2/2✓ Branch 78 → 79 taken 1 time.
✓ Branch 78 → 80 taken 1 time.
|
5 | (m_searchImap->state() == ImapService::State::Authenticated || |
| 509 | 2 | m_searchImap->state() == ImapService::State::Selected)) | |
| 510 | 2 | service = m_searchImap; | |
| 511 |
1/1✓ Branch 83 → 84 taken 12 times.
|
12 | const QPointer<ImapService> guarded = service; |
| 512 |
1/1✓ Branch 84 → 85 taken 12 times.
|
12 | const QPointer<FolderFlagSync> owner = this; |
| 513 |
2/8✓ Branch 89 → 90 taken 12 times.
✓ Branch 90 → 91 taken 12 times.
✗ Branch 115 → 116 not taken.
✗ Branch 115 → 117 not taken.
✗ Branch 118 → 119 not taken.
✗ Branch 118 → 120 not taken.
✗ Branch 121 → 122 not taken.
✗ Branch 121 → 123 not taken.
|
24 | service->executeAfterIdle([guarded, owner, account = m_folder.accountId, |
| 514 | 12 | folder = result.folderPath] { | |
| 515 |
8/8✓ Branch 3 → 4 taken 11 times.
✓ Branch 3 → 10 taken 1 time.
✓ Branch 5 → 6 taken 10 times.
✓ Branch 5 → 10 taken 1 time.
✓ Branch 8 → 9 taken 9 times.
✓ Branch 8 → 10 taken 1 time.
✓ Branch 11 → 12 taken 9 times.
✓ Branch 11 → 14 taken 3 times.
|
12 | if (guarded && owner && owner->m_folder.accountId == account) |
| 516 | 9 | guarded->statusFolder(folder); | |
| 517 | 12 | }); | |
| 518 | 12 | } | |
| 519 |
2/2✓ Branch 96 → 97 taken 11 times.
✓ Branch 96 → 102 taken 16 times.
|
27 | if (publishCount) { |
| 520 |
1/1✓ Branch 97 → 98 taken 11 times.
|
11 | m_flagIntents.setLastPolledUnread(result.folderPath, unread); |
| 521 |
2/3✓ Branch 98 → 99 taken 11 times.
✗ Branch 98 → 100 not taken.
✓ Branch 99 → 100 taken 11 times.
|
11 | if (fid >= 0) m_cache->storeBadge(fid, unread); |
| 522 |
1/1✓ Branch 100 → 101 taken 11 times.
|
11 | m_deps.cachedUnreadCountChanged(result.folderPath, unread); |
| 523 |
1/1✓ Branch 101 → 102 taken 11 times.
|
11 | m_deps.unreadCountChanged(result.folderPath, unread); |
| 524 | } | ||
| 525 | |||
| 526 | // Only advance sequential polling when NOT using NOTIFY. | ||
| 527 | // With NOTIFY, STATUS pushes arrive asynchronously (no polling loop). | ||
| 528 |
3/3✓ Branch 102 → 103 taken 27 times.
✓ Branch 103 → 104 taken 14 times.
✓ Branch 103 → 105 taken 13 times.
|
27 | if (!m_imap->isNotifying()) { |
| 529 | 14 | m_pollingIndex++; | |
| 530 |
1/1✓ Branch 104 → 105 taken 14 times.
|
14 | pollNextFolder(); |
| 531 | } | ||
| 532 | 27 | } | |
| 533 | |||
| 534 | 1 | void FolderFlagSync::onFolderStatusFailed(const QString &folder) { | |
| 535 | // F-26 (SPRINT-104): one failing STATUS (deleted elsewhere, refused) | ||
| 536 | // ends only ITS folder's update — the unread counts of every later | ||
| 537 | // folder of the round still arrive. The failing folder retries with the | ||
| 538 | // next round; its counts stay as they were. | ||
| 539 |
6/6✓ Branch 2 → 3 taken 1 time.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 13 → 4 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
|
2 | qCInfo(lcController) << "STATUS failed for" << folder |
| 540 |
1/1✓ Branch 9 → 10 taken 1 time.
|
1 | << "- continuing the polling round"; |
| 541 |
1/2✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 1 time.
|
1 | if (m_imap->isNotifying()) return; |
| 542 | 1 | ++m_pollingIndex; | |
| 543 | 1 | pollNextFolder(); | |
| 544 | } | ||
| 545 | |||
| 546 | 28 | void FolderFlagSync::startIdleIfPossible() { | |
| 547 | // T-320: Prefer NOTIFY over IDLE — watches ALL folders on one connection | ||
| 548 | // (only where NOTIFY push is allowed, see ImapService::usesNotifyPush). | ||
| 549 |
5/6✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 7 taken 27 times.
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 7 not taken.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 63 taken 27 times.
|
29 | if (m_imap->usesNotifyPush() && |
| 550 | 1 | m_imap->state() == ImapService::State::Selected) { | |
| 551 | 1 | m_imap->startNotify(m_subscribedFolders); | |
| 552 | 1 | m_pollingTimer->stop(); // NOTIFY replaces polling entirely | |
| 553 | |||
| 554 | // T-210: Log total folder-switch duration | ||
| 555 |
1/2✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 44 not taken.
|
1 | if (m_folderSwitchStopwatch.isValid()) { |
| 556 | 1 | qint64 elapsed = m_folderSwitchStopwatch.elapsed(); | |
| 557 | 2 | qCInfo(lcController) << "T-210: Folder switch total:" // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; timing path remains measured. | |
| 558 |
4/4✓ Branch 20 → 21 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 22 → 23 taken 1 time.
✓ Branch 23 → 24 taken 1 time.
|
1 | << m_folder.currentFolder << "→" << elapsed << "ms"; |
| 559 |
1/2✗ Branch 28 → 29 not taken.
✓ Branch 28 → 43 taken 1 time.
|
1 | if (elapsed > 2000) { |
| 560 | ✗ | qCWarning(lcController) << "T-210: SLOW folder switch (>2s):" // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; threshold decision remains measured. | |
| 561 | ✗ | << m_folder.currentFolder << elapsed << "ms"; | |
| 562 | } | ||
| 563 | 1 | m_folderSwitchStopwatch.invalidate(); | |
| 564 | } | ||
| 565 | |||
| 566 | 1 | m_deps.requestFolderCountStatus(); | |
| 567 |
1/1✓ Branch 47 → 48 taken 1 time.
|
2 | MailJD::Performance::FolderPerformanceTracker::instance().pushRestarted( |
| 568 |
1/1✓ Branch 48 → 49 taken 1 time.
|
2 | m_folder.currentFolderId, m_model->rowCount(), m_folder.folderGeneration); |
| 569 |
2/2✓ Branch 50 → 51 taken 1 time.
✓ Branch 51 → 52 taken 1 time.
|
1 | m_deps.authoritativeFolderPresented(m_folder.currentFolderId, m_model->rowCount(), |
| 570 | 1 | m_folder.folderGeneration); | |
| 571 | auto &backgroundTracker = | ||
| 572 | 1 | MailJD::Performance::BackgroundPerformanceTracker::instance(); | |
| 573 |
1/1✓ Branch 58 → 59 taken 1 time.
|
1 | backgroundTracker.backgroundPhaseFinished(QStringLiteral("protocol_wait"), |
| 574 |
1/1✓ Branch 54 → 55 taken 1 time.
|
1 | m_model->rowCount()); |
| 575 | 1 | backgroundTracker.backgroundFinished(true); | |
| 576 | |||
| 577 | // T-066: Trigger initial poll-equivalent via NOTIFY STATUS pushes | ||
| 578 | // (server sends STATUS for all watched folders after NOTIFY SET) | ||
| 579 | 1 | m_initialPollDone = true; | |
| 580 | 1 | return; | |
| 581 | } | ||
| 582 | |||
| 583 | // Fallback: IDLE + STATUS polling | ||
| 584 |
6/6✓ Branch 64 → 65 taken 2 times.
✓ Branch 64 → 68 taken 25 times.
✓ Branch 66 → 67 taken 1 time.
✓ Branch 66 → 68 taken 1 time.
✓ Branch 69 → 70 taken 1 time.
✓ Branch 69 → 128 taken 26 times.
|
29 | if (m_imap->hasIdleCapability() && |
| 585 | 2 | m_imap->state() == ImapService::State::Selected) { | |
| 586 | 1 | m_imap->startIdle(); | |
| 587 | |||
| 588 | // T-210: Log total folder-switch duration | ||
| 589 |
1/2✓ Branch 72 → 73 taken 1 time.
✗ Branch 72 → 104 not taken.
|
1 | if (m_folderSwitchStopwatch.isValid()) { |
| 590 | 1 | qint64 elapsed = m_folderSwitchStopwatch.elapsed(); | |
| 591 |
5/5✓ Branch 74 → 75 taken 1 time.
✓ Branch 78 → 79 taken 1 time.
✓ Branch 79 → 80 taken 1 time.
✓ Branch 87 → 76 taken 1 time.
✓ Branch 87 → 88 taken 1 time.
|
2 | qCInfo(lcController) << "T-210: Folder switch total:" |
| 592 |
4/4✓ Branch 80 → 81 taken 1 time.
✓ Branch 81 → 82 taken 1 time.
✓ Branch 82 → 83 taken 1 time.
✓ Branch 83 → 84 taken 1 time.
|
1 | << m_folder.currentFolder << "→" << elapsed << "ms"; |
| 593 |
1/2✗ Branch 88 → 89 not taken.
✓ Branch 88 → 103 taken 1 time.
|
1 | if (elapsed > 2000) { |
| 594 | ✗ | qCWarning(lcController) << "T-210: SLOW folder switch (>2s):" // GCOVR_EXCL_BR_LINE — Qt logging-macro cleanup only; threshold decision remains measured. | |
| 595 | ✗ | << m_folder.currentFolder << elapsed << "ms"; | |
| 596 | } | ||
| 597 | 1 | m_folderSwitchStopwatch.invalidate(); | |
| 598 | } | ||
| 599 | |||
| 600 | 1 | m_deps.requestFolderCountStatus(); | |
| 601 |
1/1✓ Branch 107 → 108 taken 1 time.
|
2 | MailJD::Performance::FolderPerformanceTracker::instance().pushRestarted( |
| 602 |
1/1✓ Branch 108 → 109 taken 1 time.
|
2 | m_folder.currentFolderId, m_model->rowCount(), m_folder.folderGeneration); |
| 603 |
2/2✓ Branch 110 → 111 taken 1 time.
✓ Branch 111 → 112 taken 1 time.
|
1 | m_deps.authoritativeFolderPresented(m_folder.currentFolderId, m_model->rowCount(), |
| 604 | 1 | m_folder.folderGeneration); | |
| 605 | auto &backgroundTracker = | ||
| 606 | 1 | MailJD::Performance::BackgroundPerformanceTracker::instance(); | |
| 607 |
1/1✓ Branch 118 → 119 taken 1 time.
|
1 | backgroundTracker.backgroundPhaseFinished(QStringLiteral("protocol_wait"), |
| 608 |
1/1✓ Branch 114 → 115 taken 1 time.
|
1 | m_model->rowCount()); |
| 609 | 1 | backgroundTracker.backgroundFinished(true); | |
| 610 | |||
| 611 | // Start polling timer for non-IDLE folders | ||
| 612 |
1/2✓ Branch 123 → 124 taken 1 time.
✗ Branch 123 → 128 not taken.
|
1 | if (!m_subscribedFolders.isEmpty()) { |
| 613 | 1 | m_pollingTimer->start(); | |
| 614 | |||
| 615 | // T-066: On first IDLE start, trigger an immediate poll for all | ||
| 616 | // subscribed folders so unread badges appear within seconds. | ||
| 617 |
1/2✓ Branch 125 → 126 taken 1 time.
✗ Branch 125 → 128 not taken.
|
1 | if (!m_initialPollDone) { |
| 618 | 1 | m_initialPollDone = true; | |
| 619 | // Small delay to let IDLE establish before interrupting it | ||
| 620 |
1/1✓ Branch 126 → 127 taken 1 time.
|
1 | QTimer::singleShot(2000, this, &FolderFlagSync::pollFolders); |
| 621 | } | ||
| 622 | } | ||
| 623 | } | ||
| 624 | } | ||
| 625 |