feat: stream the metadata scan instead of Collect
CopyFolder now streams envelope metadata via Next() in a first pass (dedup + queue new messages), then streams bodies for new ones in a second pass — no more blocking Collect of the whole folder with zero feedback, and memory stays flat (only new-message meta is held). - imapx: two-pass streaming CopyFolder + CopyDeps.OnScan(scanned,total) - orchestrator: throttled 'scan' events during the metadata pass - web: per-account 'scanning folder: X/N' line under the progress bar; scan events kept out of the log to avoid flooding Verified on greenmail: idempotency and internal-date preservation still hold. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMHQTtnQtQqL8muAXHr9kd
This commit is contained in:
@@ -282,6 +282,10 @@
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.pscan {
|
||||
color: var(--info);
|
||||
}
|
||||
|
||||
/* clear-log button: mirrors the .panel-label tab on the right edge */
|
||||
.log-clear {
|
||||
position: absolute;
|
||||
|
||||
@@ -15,6 +15,9 @@ type LiveProgress = {
|
||||
startTs: number
|
||||
startCount: number
|
||||
speed: number // messages/sec, averaged since the account's run started
|
||||
scanFolder?: string
|
||||
scanned?: number
|
||||
scanTotal?: number
|
||||
}
|
||||
|
||||
function fmtDuration(sec: number): string {
|
||||
@@ -89,11 +92,20 @@ export function TaskDetail({ id }: { id: number }) {
|
||||
useEffect(
|
||||
() =>
|
||||
connectTaskWS(id, (ev: TaskEvent) => {
|
||||
setLog((l) => [{ type: ev.type, text: describeEvent(ev) }, ...l].slice(0, 300))
|
||||
// `scan` is high-frequency and shown in the progress cell, not the log.
|
||||
if (ev.type !== 'scan') {
|
||||
setLog((l) => [{ type: ev.type, text: describeEvent(ev) }, ...l].slice(0, 300))
|
||||
}
|
||||
const d = (ev.data ?? {}) as Record<string, number | string | undefined>
|
||||
const accId = typeof d.account_id === 'number' ? d.account_id : undefined
|
||||
|
||||
if (ev.type === 'plan' && accId != null) {
|
||||
if (ev.type === 'scan' && accId != null) {
|
||||
setLive((prev) => {
|
||||
const cur = prev[accId]
|
||||
const base: LiveProgress = cur ?? { copied: 0, skipped: 0, total: 0, startTs: Date.now(), startCount: 0, speed: 0 }
|
||||
return { ...prev, [accId]: { ...base, scanFolder: d.folder as string | undefined, scanned: Number(d.scanned ?? 0), scanTotal: Number(d.folder_total ?? 0) } }
|
||||
})
|
||||
} else if (ev.type === 'plan' && accId != null) {
|
||||
const total = Number(d.total ?? 0)
|
||||
const now = Date.now()
|
||||
setLive((prev) => ({
|
||||
@@ -453,6 +465,7 @@ export function TaskDetail({ id }: { id: number }) {
|
||||
const done = lv.copied + lv.skipped
|
||||
const pct = Math.min(100, Math.floor((done / lv.total) * 100))
|
||||
const eta = lv.speed > 0 ? (lv.total - done) / lv.speed : Infinity
|
||||
const scanning = lv.scanned != null && lv.scanTotal != null && lv.scanned < lv.scanTotal
|
||||
return (
|
||||
<div className="acct-progress">
|
||||
<div className="pbar">
|
||||
@@ -462,6 +475,11 @@ export function TaskDetail({ id }: { id: number }) {
|
||||
{done}/{lv.total} ({pct}%) · {lv.speed >= 1 ? Math.round(lv.speed) : lv.speed.toFixed(1)}/s · ETA {fmtDuration(eta)}
|
||||
{lv.folder ? ` · ${lv.folder}` : ''}
|
||||
</span>
|
||||
{scanning && (
|
||||
<span className="pmeta pscan mono-num">
|
||||
scanning {lv.scanFolder}: {lv.scanned}/{lv.scanTotal}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
|
||||
Reference in New Issue
Block a user