返回 DeepSeek-Reasonix
useResourceOperations.ts
根目录 / desktop / frontend / src / app-runtime / useResourceOperations.ts
1 import { useLayoutEffect, useMemo, useRef } from "react";
2 import { useCommittedSlot, type CommittedSlot } from "../lib/useCommittedSlot";
3 import { CommandCancelled, executeCapturedCommand, type CommandOutcome } from "../lib/commandOutcome";
4 import { createOperationOwner, operationTargetsEqual, type OperationTarget } from "./operationOwner";
5 import { createSessionSurfaceFence, type SessionSurfaceOwnership } from "./sessionTarget";
6 import { trackAppOperation } from "./appLifecycleProbe";
7
8 export type SessionResource = Readonly<{ tabId: string; sessionKey: string }>;
9 export type SessionOperationAuthority = {
10 checkpoint(): void;
11 ownsUI(): boolean;
12 };
13 type Input = { visible: SessionResource; resources?: readonly OperationTarget[] };
14 type State = {
15 owner: ReturnType<typeof createOperationOwner>;
16 surface: ReturnType<typeof createSessionSurfaceFence>;
17 epoch: number;
18 };
19
20 function authorityFor(state: State, slot: CommittedSlot<Input>, target: OperationTarget, channel: string) {
21 const epoch = slot.epoch;
22 state.epoch = state.owner.mount();
23 const identity = state.owner.begin(target, undefined, JSON.stringify([target, channel]));
24 const surface: SessionSurfaceOwnership | undefined = state.surface.capture();
25 const authority: SessionOperationAuthority = {
26 checkpoint() {
27 if (slot.phase !== "ready" || epoch !== slot.epoch) throw new CommandCancelled("disposed");
28 if (!state.owner.owns(identity) || (slot.value?.resources && !slot.value.resources.some(resource => operationTargetsEqual(resource, target)))) {
29 throw new CommandCancelled("superseded");
30 }
31 },
32 ownsUI() {
33 try { this.checkpoint(); } catch { return false; }
34 return Boolean(surface && state.surface.owns(surface) && (target.kind !== "session" || (surface.tabId === target.tabId && surface.sessionKey === target.sessionKey)));
35 },
36 };
37 return { identity, authority };
38 }
39
40 // Stable entry is created outside render. The executor receives no capture callback.
41 function bindOperations(state: State, slot: CommittedSlot<Input>) {
42 return async <Input, Result>(
43 target: OperationTarget, channel: string, input: Input,
44 execute: (input: Input, authority: SessionOperationAuthority) => Result,
45 ): Promise<CommandOutcome<Awaited<Result>>> => {
46 if (slot.phase !== "ready" || (target.kind === "session" && !target.tabId)) return { status: "cancelled", reason: slot.phase === "disposed" ? "disposed" : "not-ready" };
47 const { identity, authority } = authorityFor(state, slot, Object.freeze({ ...target }), channel);
48 const result = await executeCapturedCommand(input, execute, authority);
49 const uiOwned = authority.ownsUI();
50 state.owner.finish(identity, result.status);
51 return result.status === "failed" && !uiOwned ? { status: "cancelled", reason: "superseded" } : result;
52 };
53 }
54
55 /** A source request may finish on A while B is visible; only its UI rights expire. */
56 export function useResourceOperations(input: Input) {
57 const slot = useCommittedSlot(input);
58 const stateRef = useRef<State | null>(null);
59 if (!stateRef.current) stateRef.current = { owner: createOperationOwner(trackAppOperation), surface: createSessionSurfaceFence(), epoch: 0 };
60 const state = stateRef.current;
61 useLayoutEffect(() => { state.surface.commit(input.visible.tabId, input.visible.sessionKey); }, [input.visible.tabId, input.visible.sessionKey, state]);
62 useLayoutEffect(() => () => {
63 state.surface.dispose();
64 state.owner.unmount(state.epoch);
65 }, [state]);
66 return useMemo(() => bindOperations(state, slot), [state, slot]);
67 }
68
68 lines TYPESCRIPT