返回 DeepSeek-Reasonix
pendingRevisionOwner.ts
根目录 / desktop / frontend / src / app-runtime / pendingRevisionOwner.ts
1 import type { SessionOperationAuthority, SessionResource, useSessionOperations } from "./useSessionOperations";
2
3 export type PendingRevisionInput = {
4 visible: SessionResource; resources: readonly SessionResource[]; running: boolean; ready: boolean;
5 operations: ReturnType<typeof useSessionOperations>;
6 send(target: SessionResource, text: string, authority: SessionOperationAuthority): Promise<void>; report(error: unknown): void;
7 };
8 type Committed = { epoch: number; input: PendingRevisionInput };
9 type Entry = { target: SessionResource; text: string; failedAt?: number; failed?: boolean };
10 const key = (target: SessionResource) => JSON.stringify([target.tabId, target.sessionKey]);
11
12 async function deliver(entry: Entry, committed: Committed) {
13 return committed.input.operations(entry.target, "plan-revision", { entry, send: committed.input.send }, async ({ entry, send }, authority) => {
14 authority.checkpoint();
15 try { await send(entry.target, entry.text, authority); } catch (error) {
16 authority.checkpoint();
17 // Resource failure retention is independent of permission to show error UI.
18 entry.failed = true;
19 throw error;
20 }
21 authority.checkpoint();
22 });
23 }
24
25 /** Latest revision per source; only an identical active request can release its slot. */
26 export function createPendingRevisionOwner(read: () => Committed | undefined) {
27 const queued = new Map<string, Entry>();
28 const active = new Map<string, Entry>();
29 let eligibility = "", eligibilityRevision = 0;
30 const pump = () => {
31 const committed = read();
32 if (!committed) return;
33 const nextEligibility = JSON.stringify([key(committed.input.visible), committed.input.running, committed.input.ready]);
34 if (eligibility !== nextEligibility) { eligibility = nextEligibility; eligibilityRevision++; }
35 const valid = new Set(committed.input.resources.map(key));
36 for (const id of queued.keys()) if (!valid.has(id)) queued.delete(id);
37 for (const id of active.keys()) if (!valid.has(id)) active.delete(id);
38 const id = key(committed.input.visible), entry = queued.get(id);
39 if (!committed.input.ready || committed.input.running || !entry || entry.failedAt === eligibilityRevision || active.has(id)) return;
40 entry.failed = false;
41 active.set(id, entry);
42 void deliver(entry, committed).then(outcome => {
43 if (read()?.epoch !== committed.epoch || active.get(id) !== entry) return;
44 if (entry.failed) {
45 // Keep the user's revision, but only a later source activation/idle
46 // transition (or a new revision) may retry it, never unrelated renders.
47 entry.failedAt = eligibilityRevision;
48 if (outcome.status === "failed") read()?.input.report(outcome.error);
49 } else if (queued.get(id) === entry) queued.delete(id);
50 }).finally(() => {
51 if (read()?.epoch !== committed.epoch || active.get(id) !== entry) return;
52 active.delete(id);
53 // A replacement revision is a new request, not a retry of the old one.
54 pump();
55 });
56 };
57 return {
58 remember(tabId: string, text: string) {
59 const committed = read();
60 const target = committed?.input.resources.find(resource => resource.tabId === tabId);
61 if (!target || !text) return;
62 queued.set(key(target), { target, text });
63 pump();
64 },
65 pump,
66 dispose() { queued.clear(); active.clear(); },
67 };
68 }
69
69 lines TYPESCRIPT