Files
webmail/lib/offline/sync-progress.ts
T
Linus Rath 8d802e4601 fix: decide on offline policy only with the whole policy and the user's groups, and ask before losing edits
A persisted account id with no subject yet read as decided: Everyone's
noOffline wiped mail a group scope allowed, and a wider scope went out at
every reload. A public, partial policy counted as the organisation's word,
and turned on the desktop default in a forbidding organisation.

- an organisation's cap on one file now binds a pin
- Clear and the off switch read what would be lost when asked, messages,
  changes and drafts, and Clear waits for a connection
- turning one account off survives the last one going off, and off then on
  no longer warns of an eviction
- a tab that takes over the store sends no scope until it has heard the policy
- a dropped account is judged by what applies to anyone
- a choice reaches the store once it settles; progress announces phases only
- restored Pro tabs ask for their titles
2026-11-03 12:33:53 +01:00

63 lines
2.3 KiB
TypeScript

/**
* How far a first sync got, and how long it may still take. The store reports
* batches of a phase: messages listed, then threads, then bodies, then files.
*/
export interface SyncProgress {
area: string;
phase: string;
done: number;
total: number | null;
/** When this phase started here, and how far it was then. */
startedAt: number;
startedDone: number;
at: number;
}
/**
* Listing a message is about ten times as fast as fetching its body and
* indexing it (bench rig, 50,000 messages: 35 s to list, 400 s in all). So
* while listing, the time left includes the bodies that come after it.
*/
const BODY_COST_PER_LIST_ITEM = 10;
/** An estimate from less than this jumps about too much to show. */
const MIN_SAMPLE_MS = 5_000;
/** A pause this long (asleep, offline) says nothing about the rate: the sample starts again. */
const SAMPLE_GAP_MS = 60_000;
/** Too small to be worth a progress line: new mail arriving in a normal pass. */
const MIN_ITEMS_SHOWN = 50;
export function nextProgress(
previous: SyncProgress | undefined,
event: { area: string; phase: string; done: number; total: number | null },
now: number,
): SyncProgress {
const samePhase = previous?.area === event.area && previous.phase === event.phase && event.done >= previous.done && now - previous.at < SAMPLE_GAP_MS;
return {
area: event.area,
phase: event.phase,
done: event.done,
total: event.total,
startedAt: samePhase ? previous.startedAt : now,
startedDone: samePhase ? previous.startedDone : event.done,
at: now,
};
}
export function progressWorthShowing(progress: SyncProgress | undefined): progress is SyncProgress {
return !!progress && (progress.total === null || progress.total >= MIN_ITEMS_SHOWN) && progress.done >= 0;
}
/** Minutes left, rounded up; null while there's nothing sensible to say. */
export function minutesLeft(progress: SyncProgress): number | null {
const { total, done, startedAt, startedDone, at } = progress;
if (total === null || total <= done) return null;
const elapsed = at - startedAt;
const moved = done - startedDone;
if (elapsed < MIN_SAMPLE_MS || moved <= 0) return null;
const perItem = elapsed / moved;
let left = (total - done) * perItem;
if (progress.phase === 'list') left += total * perItem * BODY_COST_PER_LIST_ITEM;
return Math.max(1, Math.ceil(left / 60_000));
}