1import { removeAttachmentFromServer } from "@/utils";
2import { computed, ref } from "vue";
3
4interface Uploaded {
5 name: string;
6 url: string;
7}
8
9/**
10 * Counts a composer's in-flight editor uploads so it can hold Send: a node only
11 * gets its `src` when the upload resolves, so sending early stores an empty one.
12 */
13export function useUploadTracker() {
14 const inFlight = ref(0);
15 // what this editor uploaded; the paperclip's own uploads never land here
16 const uploaded: Uploaded[] = [];
17 // the same uploads, awaited so a drop cannot run before they register
18 let landing: Promise<unknown>[] = [];
19
20 function track<T>(upload: Promise<T>): Promise<T> {
21 inFlight.value += 1;
22 landing.push(
23 upload
24 .then((file) => {
25 const { name, file_url: url } = (file ?? {}) as {
26 name?: string;
27 file_url?: string;
28 };
29 if (name && url) uploaded.push({ name, url });
30 })
31 .catch(() => {})
32 );
33 return upload.finally(() => (inFlight.value -= 1));
34 }
35
36 /**
37 * Deletes what this editor uploaded and the content no longer shows; a pasted
38 * image is attached the moment it lands. Call it where the content is final
39 * (send, discard, unmount), not on removal, which would break undo and cut.
40 */
41 async function dropUnused(content: string | null) {
42 // Discard is live during an upload, so wait for it to register or it leaks
43 await Promise.allSettled(landing);
44 landing = [];
45 const keep = content ?? "";
46 const stale = uploaded.filter((f) => !keep.includes(f.url));
47 uploaded.length = 0;
48 await Promise.allSettled(
49 stale.map((f) => removeAttachmentFromServer(f.name))
50 );
51 }
52
53 return {
54 isUploading: computed(() => inFlight.value > 0),
55 track,
56 dropUnused,
57 };
58}
59