Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
3.5 kB · 138 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139import * as tus from "tus-js-client";
// Module-level upload manager. TUS uploads live here — outside the React// tree — so navigating away from the upload screen (which unmounts it) no// longer aborts an in-flight upload. The floating UploadProgressIndicator in// the app shell subscribes to this store and renders progress until every// upload finishes.
export type UploadJob = { id: string; tid: string; filename: string; status: "uploading" | "done" | "error"; bytesSent: number; bytesTotal: number; error?: string;};
// How long a completed upload lingers in the indicator before auto-dismissing.const DONE_LINGER_MS = 4000;
let jobs: UploadJob[] = [];let nextId = 1;const listeners = new Set<() => void>();const uploads = new Map<string, tus.Upload>();
function emit() { listeners.forEach((l) => l());}
function patchJob(id: string, patch: Partial<UploadJob>) { jobs = jobs.map((j) => (j.id === id ? { ...j, ...patch } : j)); emit();}
// Warn before the tab closes while uploads are still running (web only).function beforeUnload(e: BeforeUnloadEvent) { e.preventDefault(); e.returnValue = "";}
function syncBeforeUnload() { if (typeof window === "undefined" || !window.addEventListener) return; if (jobs.some((j) => j.status === "uploading")) { window.addEventListener("beforeunload", beforeUnload); } else { window.removeEventListener("beforeunload", beforeUnload); }}
export function subscribeUploads(listener: () => void): () => void { listeners.add(listener); return () => listeners.delete(listener);}
export function getUploads(): UploadJob[] { return jobs;}
export function startUpload({ file, uploadUrl, uploadToken, tid,}: { file: File; uploadUrl: string; uploadToken: string; tid: string;}): string { const id = `upload-${nextId++}`; jobs = [ ...jobs, { id, tid, filename: file.name, status: "uploading", bytesSent: 0, bytesTotal: file.size, }, ]; emit(); syncBeforeUnload();
let retried = false; const params: tus.UploadOptions = { uploadUrl, retryDelays: [0, 1000, 3000, 5000], headers: { Authorization: `Bearer ${uploadToken}` }, metadata: { filename: file.name, filetype: file.type }, onError: (err) => { if (!retried) { retried = true; // <1mb for default nginx proxy settings params.chunkSize = 800000; doTry(); } else { console.error("upload failed", err); uploads.delete(id); patchJob(id, { status: "error", error: err instanceof Error ? err.message : String(err), }); syncBeforeUnload(); } }, onProgress: (bytesSent, bytesTotal) => { patchJob(id, { bytesSent, bytesTotal }); }, onSuccess: () => { uploads.delete(id); patchJob(id, { status: "done", bytesSent: file.size }); syncBeforeUnload(); setTimeout(() => dismissUpload(id), DONE_LINGER_MS); }, }; const doTry = () => { const upload = new tus.Upload(file, params); uploads.set(id, upload); upload.start(); }; doTry(); return id;}
export function cancelUpload(id: string) { uploads.get(id)?.abort(); uploads.delete(id); dismissUpload(id); syncBeforeUnload();}
export function dismissUpload(id: string) { if (!jobs.some((j) => j.id === id)) return; jobs = jobs.filter((j) => j.id !== id); emit();}