1import { useEffect } from "react";
2
3import { getCachedNativeReviewDiffData } from "./nativeReviewDiffAdapter";
4import type { ReviewSectionItem } from "./reviewModel";
5import {
6 getCachedReviewParsedDiff,
7 getReviewParsedDiffSourceCharacterCount,
8 MAX_CACHED_REVIEW_DIFFS,
9 MAX_CACHED_REVIEW_SOURCE_CHARACTERS,
10} from "./reviewState";
11
12interface IdleDeadlineLike {
13 readonly didTimeout: boolean;
14 timeRemaining(): number;
15}
16
17type IdleCallback = (deadline: IdleDeadlineLike) => void;
18
19function scheduleIdle(callback: IdleCallback): number {
20 if (typeof globalThis.requestIdleCallback === "function") {
21 return globalThis.requestIdleCallback(callback, { timeout: 2_000 });
22 }
23
24 return setTimeout(
25 () => callback({ didTimeout: true, timeRemaining: () => 0 }),
26 100,
27 ) as unknown as number;
28}
29
30function cancelIdle(handle: number): void {
31 if (typeof globalThis.cancelIdleCallback === "function") {
32 globalThis.cancelIdleCallback(handle);
33 return;
34 }
35 clearTimeout(handle);
36}
37
38export function prewarmReviewDiffSection(input: {
39 readonly threadKey: string;
40 readonly section: ReviewSectionItem;
41}): void {
42 const { section, threadKey } = input;
43 if (section.diff === null || section.diff.length > MAX_CACHED_REVIEW_SOURCE_CHARACTERS) {
44 return;
45 }
46
47 const parsedDiff = getCachedReviewParsedDiff({
48 threadKey,
49 sectionId: section.id,
50 diff: section.diff,
51 });
52 getCachedNativeReviewDiffData({ parsedDiff, comments: [] });
53}
54
55/** Selects nearby loaded sections that fit in the cache with the selected section. */
56export function getReviewDiffPrewarmSections(input: {
57 readonly threadKey: string;
58 readonly sections: ReadonlyArray<ReviewSectionItem>;
59 readonly selectedSectionId: string | null;
60}): ReadonlyArray<ReviewSectionItem> {
61 const { threadKey, sections, selectedSectionId } = input;
62 const selectedIndex = sections.findIndex((section) => section.id === selectedSectionId);
63 const selectedSection = sections[selectedIndex];
64 if (!selectedSection) {
65 return [];
66 }
67
68 let sourceCharacterCount = getReviewParsedDiffSourceCharacterCount({
69 threadKey,
70 sectionId: selectedSection.id,
71 diff: selectedSection.diff,
72 });
73 if (sourceCharacterCount > MAX_CACHED_REVIEW_SOURCE_CHARACTERS) {
74 return [];
75 }
76
77 const pendingSections: ReviewSectionItem[] = [];
78 for (let distance = 1; distance < sections.length; distance += 1) {
79 for (const index of [selectedIndex - distance, selectedIndex + distance]) {
80 if (pendingSections.length >= MAX_CACHED_REVIEW_DIFFS - 1) {
81 return pendingSections;
82 }
83 const section = sections[index];
84 if (!section || section.diff === null) {
85 continue;
86 }
87 const sectionCharacterCount = getReviewParsedDiffSourceCharacterCount({
88 threadKey,
89 sectionId: section.id,
90 diff: section.diff,
91 });
92 if (sourceCharacterCount + sectionCharacterCount > MAX_CACHED_REVIEW_SOURCE_CHARACTERS) {
93 continue;
94 }
95 pendingSections.push(section);
96 sourceCharacterCount += sectionCharacterCount;
97 }
98 }
99 return pendingSections;
100}
101
102/** Warms one nearby section per idle period, after navigation animations finish. */
103export function useReviewDiffPrewarming(input: {
104 readonly threadKey: string | null;
105 readonly sections: ReadonlyArray<ReviewSectionItem>;
106 readonly selectedSectionId: string | null;
107}): void {
108 const { sections, selectedSectionId, threadKey } = input;
109
110 useEffect(() => {
111 if (!threadKey) {
112 return;
113 }
114
115 const pendingSections = getReviewDiffPrewarmSections({
116 threadKey,
117 sections,
118 selectedSectionId,
119 });
120 if (pendingSections.length === 0) {
121 return;
122 }
123
124 let cancelled = false;
125 let idleHandle: number | null = null;
126 let nextSectionIndex = 0;
127
128 const scheduleNext = () => {
129 idleHandle = scheduleIdle(() => {
130 if (cancelled) {
131 return;
132 }
133
134 const section = pendingSections[nextSectionIndex];
135 if (!section) {
136 return;
137 }
138 nextSectionIndex += 1;
139 prewarmReviewDiffSection({ threadKey, section });
140
141 if (nextSectionIndex < pendingSections.length) {
142 scheduleNext();
143 }
144 });
145 };
146
147 scheduleNext();
148 return () => {
149 cancelled = true;
150 if (idleHandle !== null) {
151 cancelIdle(idleHandle);
152 }
153 };
154 }, [sections, selectedSectionId, threadKey]);
155}