authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 15:15:00-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 15:15:00-07:00
log42ff43915a2ed5c0f190019c0c1a6a1cb68658c6
tree306638903074bd725f0efe31fb10ae3d2d034760
parent834e5db45843d8b5da4fbfe1999fbaa4c58e117b
signature Signed by PGP key B5690EEEBB952194

[codex] Preserve mobile review highlighter failures (#3337)

Co-authored-by: codex <codex@users.noreply.github.com>

5 files changed, 182 insertions(+), 48 deletions(-)

apps/mobile/src/features/diffs/nativeReviewDiffHighlighter.ts+71-10
......@@ -8,6 +8,7 @@ import jsxLanguage from "@shikijs/langs/jsx";
88import tsxLanguage from "@shikijs/langs/tsx";
99import typescriptLanguage from "@shikijs/langs/typescript";
1010import yamlLanguage from "@shikijs/langs/yaml";
11import * as Schema from "effect/Schema";
1112
1213import type { NativeReviewDiffFile, NativeReviewDiffLanguage } from "./nativeReviewDiffTypes";
1314import type { NativeReviewDiffRow, NativeReviewDiffToken } from "./nativeReviewDiffSurface";
......@@ -15,6 +16,32 @@ import type { NativeReviewDiffRow, NativeReviewDiffToken } from "./nativeReviewD
1516export type NativeReviewDiffHighlightScheme = "light" | "dark";
1617export type NativeReviewDiffHighlightEngine = "native" | "javascript";
1718
19export class NativeReviewDiffHighlighterUnavailableError extends Schema.TaggedErrorClass<NativeReviewDiffHighlighterUnavailableError>()(
20 "NativeReviewDiffHighlighterUnavailableError",
21 {},
22) {
23 override get message(): string {
24 return "The native review diff highlighter is unavailable in this build.";
25 }
26}
27
28export const isNativeReviewDiffHighlighterUnavailableError = Schema.is(
29 NativeReviewDiffHighlighterUnavailableError,
30);
31
32export class NativeReviewDiffHighlighterInitializationError extends Schema.TaggedErrorClass<NativeReviewDiffHighlighterInitializationError>()(
33 "NativeReviewDiffHighlighterInitializationError",
34 {
35 requestedEngine: Schema.Literals(["native", "javascript"]),
36 attemptedEngine: Schema.Literals(["native", "javascript"]),
37 cause: Schema.Defect(),
38 },
39) {
40 override get message(): string {
41 return `Failed to initialize the ${this.attemptedEngine} review diff highlighter requested as ${this.requestedEngine}.`;
42 }
43}
44
1845export interface NativeReviewDiffHighlighterHandle {
1946 readonly engine: NativeReviewDiffHighlightEngine;
2047 readonly tokenize: (
......@@ -197,7 +224,7 @@ function normalizeTokens(
197224async function createNativeReviewDiffHighlighter(): Promise<NativeReviewDiffHighlighterHandle> {
198225 const nativeEngineModule = await import("react-native-shiki-engine");
199226 if (!nativeEngineModule.isNativeEngineAvailable()) {
200 throw new Error("Native Shiki engine is not available in this build.");
227 throw new NativeReviewDiffHighlighterUnavailableError();
201228 }
202229
203230 const highlighter = await createHighlighterCore({
......@@ -229,18 +256,52 @@ export async function getNativeReviewDiffHighlighter(
229256 engine: NativeReviewDiffHighlightEngine = "native",
230257): Promise<NativeReviewDiffHighlighterHandle> {
231258 if (engine === "javascript") {
232 javascriptHighlighterPromise ??= createJavascriptReviewDiffHighlighter();
233 return javascriptHighlighterPromise;
259 try {
260 javascriptHighlighterPromise ??= createJavascriptReviewDiffHighlighter();
261 return await javascriptHighlighterPromise;
262 } catch (cause) {
263 javascriptHighlighterPromise = null;
264 throw new NativeReviewDiffHighlighterInitializationError({
265 requestedEngine: engine,
266 attemptedEngine: "javascript",
267 cause,
268 });
269 }
234270 }
235271
236 nativeHighlighterPromise ??= createNativeReviewDiffHighlighter().catch((error: unknown) => {
237 console.warn("[debug-native-diff] native highlighter unavailable", {
238 error: error instanceof Error ? error.message : String(error),
272 nativeHighlighterPromise ??= createNativeReviewDiffHighlighter()
273 .catch(async (cause: unknown) => {
274 const nativeError = isNativeReviewDiffHighlighterUnavailableError(cause)
275 ? cause
276 : new NativeReviewDiffHighlighterInitializationError({
277 requestedEngine: engine,
278 attemptedEngine: "native",
279 cause,
280 });
281 console.warn("[debug-native-diff] native highlighter unavailable", {
282 error: nativeError,
283 });
284 try {
285 javascriptHighlighterPromise ??= createJavascriptReviewDiffHighlighter();
286 return await javascriptHighlighterPromise;
287 } catch (fallbackCause) {
288 javascriptHighlighterPromise = null;
289 throw new NativeReviewDiffHighlighterInitializationError({
290 requestedEngine: engine,
291 attemptedEngine: "javascript",
292 cause: new AggregateError(
293 [nativeError, fallbackCause],
294 "Native and JavaScript review diff highlighter initialization failed.",
295 { cause: nativeError },
296 ),
297 });
298 }
299 })
300 .catch((error) => {
301 nativeHighlighterPromise = null;
302 throw error;
239303 });
240 javascriptHighlighterPromise ??= createJavascriptReviewDiffHighlighter();
241 return javascriptHighlighterPromise;
242 });
243 return nativeHighlighterPromise;
304 return await nativeHighlighterPromise;
244305}
245306
246307function isHighlightableLineRow(row: NativeReviewDiffRow): row is NativeReviewDiffLineRow {
apps/mobile/src/features/review/reviewHighlighterState.test.ts+21-6
......@@ -53,11 +53,12 @@ it("initializes review highlighter state once", async () => {
5353});
5454
5555it("stores initialization failures in atom state", async () => {
56 const cause = new Error("load failed");
5657 const manager = createReviewHighlighterManager({
5758 getRegistry: () => registry,
5859 loader: {
5960 prepare: async () => {
60 throw new Error("load failed");
61 throw cause;
6162 },
6263 prepareLanguages: async () => undefined,
6364 getEngine: async () => "javascript",
......@@ -67,9 +68,23 @@ it("stores initialization failures in atom state", async () => {
6768 void manager.initialize();
6869 await flushAsyncWork();
6970
70 assert.deepStrictEqual(manager.getSnapshot(), {
71 engine: null,
72 error: "load failed",
73 status: "error",
74 });
71 const snapshot = manager.getSnapshot();
72 assert.strictEqual(snapshot.engine, null);
73 assert.strictEqual(snapshot.status, "error");
74 assert.strictEqual(snapshot.error?._tag, "ReviewHighlighterManagerError");
75 assert.strictEqual(snapshot.error?.operation, "prepare");
76 assert.deepStrictEqual(snapshot.error?.languages, [
77 "typescript",
78 "tsx",
79 "javascript",
80 "jsx",
81 "json",
82 "yaml",
83 "bash",
84 ]);
85 assert.strictEqual(snapshot.error?.cause, cause);
86 assert.strictEqual(
87 snapshot.error?.message,
88 "Review highlighter operation prepare failed for languages typescript, tsx, javascript, jsx, json, yaml, bash.",
89 );
7590});
apps/mobile/src/features/review/reviewHighlighterState.ts+40-15
......@@ -1,4 +1,5 @@
11import { useAtomValue } from "@effect/atom-react";
2import * as Schema from "effect/Schema";
23import { Atom, type AtomRegistry } from "effect/unstable/reactivity";
34import { useEffect } from "react";
45
......@@ -12,9 +13,22 @@ import {
1213
1314export type ReviewHighlighterStatus = "idle" | "initializing" | "ready" | "error";
1415
16export class ReviewHighlighterManagerError extends Schema.TaggedErrorClass<ReviewHighlighterManagerError>()(
17 "ReviewHighlighterManagerError",
18 {
19 operation: Schema.Literals(["prepare", "prepare-languages", "resolve-engine"]),
20 languages: Schema.Array(Schema.String),
21 cause: Schema.Defect(),
22 },
23) {
24 override get message(): string {
25 return `Review highlighter operation ${this.operation} failed for languages ${this.languages.join(", ")}.`;
26 }
27}
28
1529export interface ReviewHighlighterState {
1630 readonly engine: ReviewHighlighterEngine | null;
17 readonly error: string | null;
31 readonly error: ReviewHighlighterManagerError | null;
1832 readonly status: ReviewHighlighterStatus;
1933}
2034
......@@ -101,24 +115,35 @@ export function createReviewHighlighterManager(config: {
101115
102116 inFlight = (async () => {
103117 const startedAt = performance.now();
118 const languages = config.languages ?? REVIEW_INITIAL_LANGUAGES;
119 let operation: ReviewHighlighterManagerError["operation"] = "prepare";
120 let engine: ReviewHighlighterEngine;
104121 try {
105122 await config.loader.prepare();
106 await config.loader.prepareLanguages(config.languages ?? REVIEW_INITIAL_LANGUAGES);
107 const engine = await config.loader.getEngine();
108 const durationMs = Math.round(performance.now() - startedAt);
109 logReviewHighlighterProviderDiagnostic("initialized", {
110 durationMs,
111 engine,
123 operation = "prepare-languages";
124 await config.loader.prepareLanguages(languages);
125 operation = "resolve-engine";
126 engine = await config.loader.getEngine();
127 } catch (cause) {
128 const error = new ReviewHighlighterManagerError({
129 operation,
130 languages,
131 cause,
112132 });
113 setState({ engine, error: null, status: "ready" });
114 } catch (error) {
115 const message = error instanceof Error ? error.message : String(error);
116 logReviewHighlighterProviderDiagnostic("initialization failed", { error: message });
117 setState({ engine: null, error: message, status: "error" });
118 } finally {
119 inFlight = null;
133 logReviewHighlighterProviderDiagnostic("initialization failed", { error });
134 setState({ engine: null, error, status: "error" });
135 return;
120136 }
121 })();
137
138 const durationMs = Math.round(performance.now() - startedAt);
139 logReviewHighlighterProviderDiagnostic("initialized", {
140 durationMs,
141 engine,
142 });
143 setState({ engine, error: null, status: "ready" });
144 })().finally(() => {
145 inFlight = null;
146 });
122147
123148 return inFlight;
124149 }
apps/mobile/src/features/review/shikiReviewHighlighter.ts+45-16
......@@ -10,6 +10,7 @@ import yamlLanguage from "@shikijs/langs/yaml";
1010import githubDarkDefault from "@shikijs/themes/github-dark-default";
1111import githubLightDefault from "@shikijs/themes/github-light-default";
1212import { getFiletypeFromFileName } from "@pierre/diffs/utils/getFiletypeFromFileName";
13import * as Schema from "effect/Schema";
1314
1415import {
1516 resolveReviewHighlighterEngine,
......@@ -22,6 +23,19 @@ import { applyDiffRangesToTokens, computeWordAltDiffRanges } from "./reviewWordD
2223export type ReviewDiffTheme = "light" | "dark";
2324export type { ReviewHighlighterEngine };
2425
26export class ReviewHighlighterEngineInitializationError extends Schema.TaggedErrorClass<ReviewHighlighterEngineInitializationError>()(
27 "ReviewHighlighterEngineInitializationError",
28 {
29 preferredEngine: Schema.Literals(["native", "javascript"]),
30 attemptedEngine: Schema.Literals(["native", "javascript"]),
31 cause: Schema.Defect(),
32 },
33) {
34 override get message(): string {
35 return `Failed to initialize the ${this.attemptedEngine} review highlighter with ${this.preferredEngine} preferred.`;
36 }
37}
38
2539export interface ReviewHighlightedToken {
2640 content: string;
2741 readonly color: string | null;
......@@ -227,16 +241,6 @@ function logReviewHighlighterDiagnosticError(message: string, error: unknown): v
227241 if (!isReviewHighlighterDebugLoggingEnabled()) {
228242 return;
229243 }
230
231 if (error instanceof Error) {
232 console.error(`[review-highlighter] ${message}`, {
233 name: error.name,
234 message: error.message,
235 stack: error.stack,
236 });
237 return;
238 }
239
240244 console.error(`[review-highlighter] ${message}`, error);
241245}
242246
......@@ -258,6 +262,7 @@ async function getHighlighter(): Promise<HighlighterCore> {
258262 if (!highlighterPromise) {
259263 const configuredHighlighterPromise = (async () => {
260264 let nativeEngineAvailable = false;
265 let nativeInitializationError: ReviewHighlighterEngineInitializationError | undefined;
261266
262267 logReviewHighlighterDiagnostic("initializing", {
263268 configuredPreference: REVIEW_HIGHLIGHTER_ENGINE_ENV_VALUE,
......@@ -289,9 +294,14 @@ async function getHighlighter(): Promise<HighlighterCore> {
289294 };
290295 }
291296 } catch (error) {
297 nativeInitializationError = new ReviewHighlighterEngineInitializationError({
298 preferredEngine: REVIEW_HIGHLIGHTER_ENGINE_PREFERENCE,
299 attemptedEngine: "native",
300 cause: error,
301 });
292302 logReviewHighlighterDiagnosticError(
293303 "native engine initialization failed; falling back to javascript",
294 error,
304 nativeInitializationError,
295305 );
296306 nativeEngineAvailable = false;
297307 }
......@@ -305,11 +315,30 @@ async function getHighlighter(): Promise<HighlighterCore> {
305315 REVIEW_HIGHLIGHTER_ENGINE_PREFERENCE,
306316 nativeEngineAvailable,
307317 );
308 const highlighter = await createHighlighterCore({
309 themes,
310 langs: REVIEW_INITIAL_LANGUAGE_MODULES,
311 engine: createJavaScriptRegexEngine(),
312 });
318 let highlighter: HighlighterCore;
319 try {
320 highlighter = await createHighlighterCore({
321 themes,
322 langs: REVIEW_INITIAL_LANGUAGE_MODULES,
323 engine: createJavaScriptRegexEngine(),
324 });
325 } catch (cause) {
326 const javascriptError = new ReviewHighlighterEngineInitializationError({
327 preferredEngine: REVIEW_HIGHLIGHTER_ENGINE_PREFERENCE,
328 attemptedEngine: "javascript",
329 cause,
330 });
331 if (!nativeInitializationError) throw javascriptError;
332 throw new ReviewHighlighterEngineInitializationError({
333 preferredEngine: REVIEW_HIGHLIGHTER_ENGINE_PREFERENCE,
334 attemptedEngine: "javascript",
335 cause: new AggregateError(
336 [nativeInitializationError, javascriptError],
337 "Native and JavaScript review highlighter initialization failed.",
338 { cause: nativeInitializationError },
339 ),
340 });
341 }
313342 logReviewHighlighterDiagnostic("using javascript engine", {
314343 resolvedEngine: engine,
315344 });
apps/mobile/src/features/review/useNativeReviewDiffHighlighting.ts+5-1
......@@ -108,7 +108,11 @@ export function useNativeReviewDiffHighlighting(input: {
108108 } catch (error) {
109109 if (!abortController.signal.aborted) {
110110 logReviewDiffDiagnostic("native visible highlight failed", {
111 error: error instanceof Error ? error.message : String(error),
111 error,
112 resetKey,
113 scheme,
114 firstRowIndex: requestRange.firstRowIndex,
115 lastRowIndex: requestRange.lastRowIndex,
112116 });
113117 }
114118 }