1import type { QueuedThreadMessage } from "./thread-outbox";
2import { threadOutboxRevision, updateThreadOutboxMessage } from "./thread-outbox";
3import { getComposerDraftSnapshot, sameComposerDraftState } from "./use-composer-drafts";
4
5type PendingTaskEditorWriteResult =
6 | {
7 readonly status: "complete";
8 readonly updated: boolean;
9 readonly nextRevision: number;
10 }
11 | {
12 readonly status: "failed";
13 readonly error: unknown;
14 readonly nextRevision: number;
15 };
16
17const pendingWrites = new Map<
18 QueuedThreadMessage["messageId"],
19 Promise<PendingTaskEditorWriteResult>
20>();
21
22/**
23 * Captures this editor's outbox revision and any editor save it must follow.
24 * The returned promise keeps that predecessor even after its map entry clears.
25 */
26export function capturePendingTaskEditorWriteBaseline(
27 messageId: QueuedThreadMessage["messageId"],
28): Promise<number> {
29 const capturedRevision = threadOutboxRevision(messageId);
30 const predecessor = pendingWrites.get(messageId);
31 if (!predecessor) {
32 return Promise.resolve(capturedRevision);
33 }
34 return predecessor.then(
35 ({ nextRevision }) => Math.max(capturedRevision, nextRevision),
36 () => capturedRevision,
37 );
38}
39
40/**
41 * Saves one dismissed editor after its captured predecessor. A true result
42 * means both the outbox write and this editor's draft snapshot still match.
43 */
44export function flushPendingTaskEditorWrite(input: {
45 readonly message: QueuedThreadMessage;
46 readonly baseline: Promise<number>;
47 readonly draftKey: string;
48}): Promise<boolean> {
49 const { message } = input;
50 const draftSnapshot = getComposerDraftSnapshot(input.draftKey);
51 const write = input.baseline.then(
52 async (expectedRevision): Promise<PendingTaskEditorWriteResult> => {
53 try {
54 const updated = await updateThreadOutboxMessage(message, expectedRevision);
55 return {
56 status: "complete",
57 updated,
58 nextRevision: expectedRevision + (updated ? 1 : 0),
59 };
60 } catch (error) {
61 // A failed write does not advance the outbox, but later editor saves
62 // still need the expected revision handed off by its predecessor.
63 return {
64 status: "failed",
65 error,
66 nextRevision: expectedRevision,
67 };
68 }
69 },
70 );
71
72 pendingWrites.set(message.messageId, write);
73 const removeWrite = (): void => {
74 if (pendingWrites.get(message.messageId) === write) {
75 pendingWrites.delete(message.messageId);
76 }
77 };
78 void write.then(removeWrite, removeWrite);
79
80 return write.then((result) => {
81 if (result.status === "failed") {
82 throw result.error;
83 }
84 return (
85 result.updated &&
86 sameComposerDraftState(draftSnapshot, getComposerDraftSnapshot(input.draftKey))
87 );
88 });
89}