1import type { EnvironmentId } from "@t3tools/contracts";
2
3export type ReadyAttachmentUpload = {
4 readonly status: "ready";
5 readonly environmentId: EnvironmentId;
6 readonly attachmentId: string;
7};
8
9export type AttachmentUploadState =
10 | {
11 readonly status: "uploading";
12 readonly environmentId: EnvironmentId;
13 readonly progress: number;
14 readonly previous?: ReadyAttachmentUpload;
15 }
16 | ReadyAttachmentUpload
17 | {
18 readonly status: "failed";
19 readonly environmentId: EnvironmentId;
20 readonly reason: string;
21 /**
22 * A pending upload the queue minted for this failed attempt. Retry and
23 * release delete it, so it must never carry a draft's persisted
24 * attachment id.
25 */
26 readonly attachmentId?: string;
27 readonly previous?: ReadyAttachmentUpload;
28 };
29
30export function attachmentUploadBlockReason(input: {
31 readonly imageIds: ReadonlyArray<string>;
32 readonly uploadsByImageId: Readonly<Record<string, AttachmentUploadState>>;
33 readonly environmentId: EnvironmentId;
34}): string | null {
35 let pending = 0;
36 let failed = 0;
37
38 for (const imageId of input.imageIds) {
39 const upload = input.uploadsByImageId[imageId];
40 if (upload?.status === "failed" && upload.environmentId === input.environmentId) {
41 failed += 1;
42 } else if (upload?.status !== "ready" || upload.environmentId !== input.environmentId) {
43 pending += 1;
44 }
45 }
46
47 if (failed > 0) {
48 return failed === 1
49 ? "Retry or remove the failed attachment"
50 : "Retry or remove the failed attachments";
51 }
52 if (pending > 0) {
53 return pending === 1 ? "Attachment still uploading" : "Attachments still uploading";
54 }
55 return null;
56}
57
58export function formatAttachmentUploadProgress(progress: number): string {
59 const bounded = Math.max(0, Math.min(1, Number.isFinite(progress) ? progress : 0));
60 return `${Math.floor(bounded * 100)}%`;
61}