Skip to content

Commit 665c0c6

Browse files
Refactor mobile remote search and review state
- Move composer path search and source-control discovery into shared managed state - Persist review async/loading state in atoms and add coverage - Update mobile/web composers and review highlighter initialization
1 parent 89fbeb2 commit 665c0c6

37 files changed

Lines changed: 1366 additions & 546 deletions

.oxfmtrc.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
"bun.lock",
1010
"*.tsbuildinfo",
1111
"**/routeTree.gen.ts",
12+
"apps/mobile/android/**",
13+
"apps/mobile/ios/**",
1214
"apps/web/public/mockServiceWorker.js",
1315
"apps/web/src/lib/vendor/qrcodegen.ts",
1416
"apps/mobile/uniwind-types.d.ts",

.oxlintrc.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
"bun.lock",
88
"*.tsbuildinfo",
99
"**/routeTree.gen.ts",
10+
"apps/mobile/android/**",
11+
"apps/mobile/ios/**",
1012
"apps/mobile/uniwind-types.d.ts"
1113
],
1214
"plugins": ["eslint", "oxc", "react", "unicorn", "typescript"],
Lines changed: 5 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,15 @@
1-
import { createContext, type ReactNode, useContext, useEffect, useMemo, useState } from "react";
1+
import { createContext, type ReactNode, useContext, useMemo } from "react";
22

3-
import {
4-
getActiveReviewHighlighterEngine,
5-
prepareReviewHighlighter,
6-
prepareReviewHighlighterLanguages,
7-
type ReviewHighlighterEngine,
8-
} from "./shikiReviewHighlighter";
3+
import { type ReviewHighlighterState, useReviewHighlighterState } from "./reviewHighlighterState";
94

10-
type ReviewHighlighterStatus = "idle" | "initializing" | "ready" | "error";
11-
12-
interface ReviewHighlighterContextValue {
13-
readonly engine: ReviewHighlighterEngine | null;
14-
readonly error: string | null;
15-
readonly status: ReviewHighlighterStatus;
16-
}
17-
18-
const ReviewHighlighterContext = createContext<ReviewHighlighterContextValue>({
5+
const ReviewHighlighterContext = createContext<ReviewHighlighterState>({
196
engine: null,
207
error: null,
218
status: "idle",
229
});
2310

24-
const REVIEW_INITIAL_LANGUAGES = [
25-
"typescript",
26-
"tsx",
27-
"javascript",
28-
"jsx",
29-
"json",
30-
"yaml",
31-
"bash",
32-
] as const;
33-
34-
function isReviewHighlighterProviderDebugLoggingEnabled(): boolean {
35-
return typeof __DEV__ !== "undefined" ? __DEV__ : false;
36-
}
37-
38-
function logReviewHighlighterProviderDiagnostic(
39-
message: string,
40-
details?: Record<string, unknown>,
41-
): void {
42-
if (!isReviewHighlighterProviderDebugLoggingEnabled()) {
43-
return;
44-
}
45-
46-
if (details) {
47-
console.log(`[review-highlighter-provider] ${message}`, details);
48-
return;
49-
}
50-
51-
console.log(`[review-highlighter-provider] ${message}`);
52-
}
53-
5411
export function ReviewHighlighterProvider(props: { readonly children: ReactNode }) {
55-
const [value, setValue] = useState<ReviewHighlighterContextValue>({
56-
engine: null,
57-
error: null,
58-
status: "idle",
59-
});
60-
61-
useEffect(() => {
62-
let cancelled = false;
63-
64-
setValue({ engine: null, error: null, status: "initializing" });
65-
66-
void (async () => {
67-
const startedAt = performance.now();
68-
try {
69-
await prepareReviewHighlighter();
70-
await prepareReviewHighlighterLanguages(REVIEW_INITIAL_LANGUAGES);
71-
const engine = await getActiveReviewHighlighterEngine();
72-
73-
if (cancelled) {
74-
return;
75-
}
76-
77-
const durationMs = Math.round(performance.now() - startedAt);
78-
logReviewHighlighterProviderDiagnostic("initialized", {
79-
durationMs,
80-
engine,
81-
});
82-
setValue({ engine, error: null, status: "ready" });
83-
} catch (error) {
84-
if (cancelled) {
85-
return;
86-
}
87-
88-
const message = error instanceof Error ? error.message : String(error);
89-
logReviewHighlighterProviderDiagnostic("initialization failed", { error: message });
90-
setValue({ engine: null, error: message, status: "error" });
91-
}
92-
})();
93-
94-
return () => {
95-
cancelled = true;
96-
};
97-
}, []);
98-
12+
const value = useReviewHighlighterState();
9913
const contextValue = useMemo(() => value, [value]);
10014

10115
return (
@@ -105,6 +19,6 @@ export function ReviewHighlighterProvider(props: { readonly children: ReactNode
10519
);
10620
}
10721

108-
export function useReviewHighlighterStatus(): ReviewHighlighterContextValue {
22+
export function useReviewHighlighterStatus(): ReviewHighlighterState {
10923
return useContext(ReviewHighlighterContext);
11024
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { assert, beforeEach, it } from "vitest";
2+
import { AtomRegistry } from "effect/unstable/reactivity";
3+
4+
import {
5+
createReviewHighlighterManager,
6+
IDLE_REVIEW_HIGHLIGHTER_STATE,
7+
} from "./reviewHighlighterState";
8+
9+
let registry = AtomRegistry.make();
10+
11+
beforeEach(() => {
12+
registry.dispose();
13+
registry = AtomRegistry.make();
14+
});
15+
16+
function flushAsyncWork(): Promise<void> {
17+
return Promise.resolve().then(() => undefined);
18+
}
19+
20+
it("initializes review highlighter state once", async () => {
21+
let prepareCalls = 0;
22+
let languageCalls = 0;
23+
let engineCalls = 0;
24+
const manager = createReviewHighlighterManager({
25+
getRegistry: () => registry,
26+
loader: {
27+
prepare: async () => {
28+
prepareCalls += 1;
29+
},
30+
prepareLanguages: async () => {
31+
languageCalls += 1;
32+
},
33+
getEngine: async () => {
34+
engineCalls += 1;
35+
return "javascript";
36+
},
37+
},
38+
});
39+
40+
assert.deepStrictEqual(manager.getSnapshot(), IDLE_REVIEW_HIGHLIGHTER_STATE);
41+
42+
await Promise.all([manager.initialize(), manager.initialize()]);
43+
await manager.initialize();
44+
45+
assert.strictEqual(prepareCalls, 1);
46+
assert.strictEqual(languageCalls, 1);
47+
assert.strictEqual(engineCalls, 1);
48+
assert.deepStrictEqual(manager.getSnapshot(), {
49+
engine: "javascript",
50+
error: null,
51+
status: "ready",
52+
});
53+
});
54+
55+
it("stores initialization failures in atom state", async () => {
56+
const manager = createReviewHighlighterManager({
57+
getRegistry: () => registry,
58+
loader: {
59+
prepare: async () => {
60+
throw new Error("load failed");
61+
},
62+
prepareLanguages: async () => undefined,
63+
getEngine: async () => "javascript",
64+
},
65+
});
66+
67+
void manager.initialize();
68+
await flushAsyncWork();
69+
70+
assert.deepStrictEqual(manager.getSnapshot(), {
71+
engine: null,
72+
error: "load failed",
73+
status: "error",
74+
});
75+
});
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { useAtomValue } from "@effect/atom-react";
2+
import { Atom, type AtomRegistry } from "effect/unstable/reactivity";
3+
import { useEffect } from "react";
4+
5+
import { appAtomRegistry } from "../../state/atom-registry";
6+
import {
7+
getActiveReviewHighlighterEngine,
8+
prepareReviewHighlighter,
9+
prepareReviewHighlighterLanguages,
10+
type ReviewHighlighterEngine,
11+
} from "./shikiReviewHighlighter";
12+
13+
export type ReviewHighlighterStatus = "idle" | "initializing" | "ready" | "error";
14+
15+
export interface ReviewHighlighterState {
16+
readonly engine: ReviewHighlighterEngine | null;
17+
readonly error: string | null;
18+
readonly status: ReviewHighlighterStatus;
19+
}
20+
21+
export interface ReviewHighlighterLoader {
22+
readonly prepare: () => Promise<void>;
23+
readonly prepareLanguages: (languages: ReadonlyArray<string>) => Promise<void>;
24+
readonly getEngine: () => Promise<ReviewHighlighterEngine>;
25+
}
26+
27+
const REVIEW_INITIAL_LANGUAGES = [
28+
"typescript",
29+
"tsx",
30+
"javascript",
31+
"jsx",
32+
"json",
33+
"yaml",
34+
"bash",
35+
] as const;
36+
37+
export const IDLE_REVIEW_HIGHLIGHTER_STATE = Object.freeze<ReviewHighlighterState>({
38+
engine: null,
39+
error: null,
40+
status: "idle",
41+
});
42+
43+
const INITIALIZING_REVIEW_HIGHLIGHTER_STATE = Object.freeze<ReviewHighlighterState>({
44+
engine: null,
45+
error: null,
46+
status: "initializing",
47+
});
48+
49+
export const reviewHighlighterStateAtom = Atom.make(IDLE_REVIEW_HIGHLIGHTER_STATE).pipe(
50+
Atom.keepAlive,
51+
Atom.withLabel("mobile:review-highlighter"),
52+
);
53+
54+
function isReviewHighlighterProviderDebugLoggingEnabled(): boolean {
55+
return typeof __DEV__ !== "undefined" ? __DEV__ : false;
56+
}
57+
58+
function logReviewHighlighterProviderDiagnostic(
59+
message: string,
60+
details?: Record<string, unknown>,
61+
): void {
62+
if (!isReviewHighlighterProviderDebugLoggingEnabled()) {
63+
return;
64+
}
65+
66+
if (details) {
67+
console.log(`[review-highlighter-provider] ${message}`, details);
68+
return;
69+
}
70+
71+
console.log(`[review-highlighter-provider] ${message}`);
72+
}
73+
74+
export function createReviewHighlighterManager(config: {
75+
readonly getRegistry: () => AtomRegistry.AtomRegistry;
76+
readonly loader: ReviewHighlighterLoader;
77+
readonly languages?: ReadonlyArray<string>;
78+
}) {
79+
let started = false;
80+
let inFlight: Promise<void> | null = null;
81+
82+
function getSnapshot(): ReviewHighlighterState {
83+
return config.getRegistry().get(reviewHighlighterStateAtom);
84+
}
85+
86+
function setState(state: ReviewHighlighterState): void {
87+
config.getRegistry().set(reviewHighlighterStateAtom, state);
88+
}
89+
90+
function initialize(): Promise<void> {
91+
if (inFlight) {
92+
return inFlight;
93+
}
94+
95+
if (started && getSnapshot().status === "ready") {
96+
return Promise.resolve();
97+
}
98+
99+
started = true;
100+
setState(INITIALIZING_REVIEW_HIGHLIGHTER_STATE);
101+
102+
inFlight = (async () => {
103+
const startedAt = performance.now();
104+
try {
105+
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,
112+
});
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;
120+
}
121+
})();
122+
123+
return inFlight;
124+
}
125+
126+
function reset(): void {
127+
started = false;
128+
inFlight = null;
129+
setState(IDLE_REVIEW_HIGHLIGHTER_STATE);
130+
}
131+
132+
return {
133+
getSnapshot,
134+
initialize,
135+
reset,
136+
};
137+
}
138+
139+
const reviewHighlighterManager = createReviewHighlighterManager({
140+
getRegistry: () => appAtomRegistry,
141+
loader: {
142+
prepare: prepareReviewHighlighter,
143+
prepareLanguages: prepareReviewHighlighterLanguages,
144+
getEngine: getActiveReviewHighlighterEngine,
145+
},
146+
});
147+
148+
export function useReviewHighlighterState(): ReviewHighlighterState {
149+
useEffect(() => {
150+
void reviewHighlighterManager.initialize();
151+
}, []);
152+
153+
return useAtomValue(reviewHighlighterStateAtom);
154+
}

apps/mobile/src/features/review/reviewModel.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { parsePatchFiles, type ChangeTypes, type FileDiffMetadata } from "@pierre/diffs/utils";
1+
import type { ChangeTypes, FileDiffMetadata } from "@pierre/diffs/types";
2+
import { parsePatchFiles } from "@pierre/diffs/utils/parsePatchFiles";
23
import type { OrchestrationCheckpointSummary, ReviewDiffPreviewSource } from "@t3tools/contracts";
34
import * as Arr from "effect/Array";
45
import * as Order from "effect/Order";

0 commit comments

Comments
 (0)