返回 DeepSeek-Reasonix
navigationSurfaceTransition.ts
根目录 / desktop / frontend / src / lib / navigationSurfaceTransition.ts
1 export type SurfaceDataOutcome = "ready" | "failed" | "cancelled" | "superseded";
2
3 export type SurfaceDataCommit = {
4 intent: number;
5 outcome: SurfaceDataOutcome;
6 tabId?: string;
7 surfaceKey?: string;
8 error?: string;
9 };
10
11 export type NavigationResult<T> = {
12 value: T;
13 surfaceReady: Promise<SurfaceDataCommit>;
14 };
15
16 export type NavigationSurfaceState = null | {
17 intent: number;
18 phase: "source-retained" | "target-masked";
19 targetTabId?: string;
20 targetSurfaceKey?: string;
21 };
22
23 export type SurfacePaintProgress = { attempts: number; stableFrames: number; geometryKey?: string };
24 export type SurfacePaintDecision = {
25 progress: SurfacePaintProgress;
26 outcome?: "ready" | "degraded";
27 requestRecovery: boolean;
28 };
29
30 /** Deterministic paint gate shared by Transcript and its fake-clock tests. */
31 export function advanceSurfacePaintCommit(
32 current: SurfacePaintProgress,
33 sample: { rendered: boolean; placementReady: boolean; geometryReady: boolean; geometryKey?: string },
34 ): SurfacePaintDecision {
35 const attempts = current.attempts + 1;
36 const ready = sample.rendered && sample.placementReady && sample.geometryReady && Boolean(sample.geometryKey);
37 const stableFrames = ready
38 ? (current.geometryKey === sample.geometryKey ? current.stableFrames + 1 : 1)
39 : 0;
40 const geometryKey = ready ? sample.geometryKey : undefined;
41 if (stableFrames >= 2) {
42 return { progress: { attempts, stableFrames, geometryKey }, outcome: "ready", requestRecovery: false };
43 }
44 if (attempts >= 180) {
45 return { progress: { attempts, stableFrames, geometryKey }, outcome: "degraded", requestRecovery: false };
46 }
47 return {
48 progress: { attempts, stableFrames, geometryKey },
49 requestRecovery: attempts === 60 || attempts === 120,
50 };
51 }
52
53 export type NavigationSurfaceIntent = number | null;
54
55 export type NavigationSurfaceTicket = Readonly<{
56 token: string;
57 intent: number;
58 targetTabId: string;
59 targetSessionKey: string;
60 }>;
61
62 let nextPaintReceipt = 0;
63
64 /** Opaque public token plus the complete internal target identity. */
65 export function createNavigationSurfaceTicket(
66 intent: number,
67 targetTabId: string,
68 targetSessionKey: string,
69 ): NavigationSurfaceTicket {
70 return Object.freeze({
71 token: `navigation-${intent}-${++nextPaintReceipt}`,
72 intent,
73 targetTabId,
74 targetSessionKey,
75 });
76 }
77
78 export function matchesNavigationSurfaceTicket(
79 ticket: NavigationSurfaceTicket | null,
80 token: string,
81 intent: number | null,
82 targetTabId: string | undefined,
83 targetSessionKey: string,
84 ): boolean {
85 return Boolean(
86 ticket
87 && intent !== null
88 && ticket.token === token
89 && ticket.intent === intent
90 && ticket.targetTabId === targetTabId
91 && ticket.targetSessionKey === targetSessionKey,
92 );
93 }
94
95 export function beginNavigationSurfaceState(intent: number): NavigationSurfaceState {
96 return { intent, phase: "source-retained" };
97 }
98
99 /** The bridge navigation call returned; target data may still be hydrating. */
100 export function markNavigationTargetMasked(
101 current: NavigationSurfaceState,
102 intent: number,
103 targetTabId?: string,
104 targetSurfaceKey?: string,
105 ): NavigationSurfaceState {
106 if (current?.intent !== intent) return current;
107 return { ...current, phase: "target-masked", targetTabId, targetSurfaceKey };
108 }
109
110 /** Only a target paint terminal may release the opaque surface mask. */
111 export function settleNavigationSurfaceState(
112 current: NavigationSurfaceState,
113 completedIntent: number,
114 ): NavigationSurfaceState {
115 return current?.intent === completedIntent ? null : current;
116 }
117
118 /** Older completions must never release the latest navigation surface mask. */
119 export function settleNavigationSurfaceIntent(
120 current: NavigationSurfaceIntent,
121 completedIntent: number,
122 ): NavigationSurfaceIntent {
123 return current === completedIntent ? null : current;
124 }
125
126 type BackendNavigationResultGuard = {
127 intent: number;
128 targetTabId: string;
129 kind: string;
130 isIntentCurrent: (intent: number) => boolean;
131 reassert: (kind: string, staleTabId: string) => Promise<void>;
132 };
133
134 /**
135 * Backend reveal calls activate their returned tab before resolving. A stale
136 * frontend result therefore needs an active repair, not just an ignored value.
137 */
138 export async function guardBackendNavigationResult({
139 intent,
140 targetTabId,
141 kind,
142 isIntentCurrent,
143 reassert,
144 }: BackendNavigationResultGuard): Promise<boolean> {
145 if (isIntentCurrent(intent)) return true;
146 await reassert(kind, targetTabId);
147 return false;
148 }
149
149 lines TYPESCRIPT