* perf(timeline): resume cached history from its exact range Warm launches were repainting cached history and then downloading the same bounded tail to rediscover that nothing changed. Persist synchronization authority only when the complete current canonical window can be encoded losslessly; otherwise retain the existing display-only fallback. * test(timeline): hide replica setup behind E2E helper * fix(timeline): bound persisted resume overflow * test(timeline): cover workspace eviction catch-up order * test(timeline): read persisted range from IndexedDB
102 lines
2.5 KiB
TypeScript
102 lines
2.5 KiB
TypeScript
import { TIMELINE_FETCH_PAGE_SIZE } from "@/timeline/timeline-fetch-policy";
|
|
|
|
export interface TimelineSyncCursor {
|
|
epoch: string;
|
|
seq: number;
|
|
}
|
|
|
|
export interface ProjectedTimelineTailFetchPlan {
|
|
direction: "tail";
|
|
limit: number;
|
|
projection: "projected";
|
|
}
|
|
|
|
export interface ProjectedTimelineAfterFetchPlan {
|
|
direction: "after";
|
|
cursor: TimelineSyncCursor;
|
|
limit: number;
|
|
projection: "projected";
|
|
}
|
|
|
|
export interface ProjectedTimelineBeforeFetchPlan {
|
|
direction: "before";
|
|
cursor: TimelineSyncCursor;
|
|
limit: number;
|
|
projection: "projected";
|
|
}
|
|
|
|
export type ProjectedTimelineFetchPlan =
|
|
| ProjectedTimelineTailFetchPlan
|
|
| ProjectedTimelineAfterFetchPlan
|
|
| ProjectedTimelineBeforeFetchPlan;
|
|
|
|
export type ProjectedTimelineForwardFetchPlan =
|
|
| ProjectedTimelineTailFetchPlan
|
|
| ProjectedTimelineAfterFetchPlan;
|
|
|
|
export function planTimelineCatchUpAfter(cursor: TimelineSyncCursor) {
|
|
return {
|
|
direction: "after",
|
|
cursor,
|
|
limit: TIMELINE_FETCH_PAGE_SIZE,
|
|
projection: "projected",
|
|
} as const;
|
|
}
|
|
|
|
export function planTimelineTailFetch() {
|
|
return {
|
|
direction: "tail",
|
|
limit: TIMELINE_FETCH_PAGE_SIZE,
|
|
projection: "projected",
|
|
} as const;
|
|
}
|
|
|
|
export function planTimelineResumeFetch(
|
|
range: { epoch: string; endSeq: number } | null | undefined,
|
|
): ProjectedTimelineForwardFetchPlan {
|
|
return range
|
|
? planTimelineCatchUpAfter({ epoch: range.epoch, seq: range.endSeq })
|
|
: planTimelineTailFetch();
|
|
}
|
|
|
|
export function planTimelineOlderFetch(cursor: TimelineSyncCursor) {
|
|
return {
|
|
direction: "before",
|
|
cursor,
|
|
limit: TIMELINE_FETCH_PAGE_SIZE,
|
|
projection: "projected",
|
|
} as const;
|
|
}
|
|
|
|
export function planTimelinePromptJump(target: TimelineSyncCursor) {
|
|
const newerRows = Math.floor(TIMELINE_FETCH_PAGE_SIZE / 2);
|
|
return {
|
|
direction: "before",
|
|
cursor: { epoch: target.epoch, seq: target.seq + newerRows + 1 },
|
|
limit: TIMELINE_FETCH_PAGE_SIZE,
|
|
projection: "projected",
|
|
mergeWindow: true,
|
|
} as const;
|
|
}
|
|
|
|
export function isTimelineCatchUpComplete(input: {
|
|
direction: "tail" | "before" | "after";
|
|
hasNewer: boolean;
|
|
error: string | null;
|
|
}): boolean {
|
|
if (input.error) {
|
|
return false;
|
|
}
|
|
|
|
return input.direction !== "after" || !input.hasNewer;
|
|
}
|
|
|
|
export function isTimelineResumeSnapshotAuthoritative(input: {
|
|
direction: "tail" | "before" | "after";
|
|
hasNewer: boolean;
|
|
error: string | null;
|
|
}): boolean {
|
|
if (input.error || input.direction === "before") return false;
|
|
return input.direction === "tail" || !input.hasNewer;
|
|
}
|