queued downloads and stream not buffer of downloads

This commit is contained in:
2025-09-23 12:45:20 +01:00
parent cb948a160b
commit 0b3cb9778b
6 changed files with 648 additions and 137 deletions
+48
View File
@@ -0,0 +1,48 @@
import { useState, useCallback } from "react";
export function useDownloadQueue(maxConcurrent = 3) {
const [queue, setQueue] = useState([]); // queued tasks
const [activeCount, setActiveCount] = useState(0);
const [statuses, setStatuses] = useState({}); // { id: "idle"|"queued"|"downloading"|"done"|"failed" }
const scheduleNext = useCallback(() => {
setQueue((queueCurr) => {
if (activeCount >= maxConcurrent || queueCurr.length === 0)
return queueCurr;
const [task, ...rest] = queueCurr;
setStatuses((s) => ({ ...s, [task.id]: "downloading" }));
setActiveCount((c) => c + 1);
// Run the download fully in async IIFE
(async () => {
try {
await task.downloadFn(); // triggers fetch + blob + <a> click
setStatuses((s) => ({ ...s, [task.id]: "done" }));
} catch (err) {
console.error(err);
setStatuses((s) => ({ ...s, [task.id]: "failed" }));
} finally {
setActiveCount((c) => c - 1);
scheduleNext(); // promote next in queue
}
})();
return rest;
});
}, [activeCount, maxConcurrent]);
const startDownload = useCallback(
(id, downloadFn) => {
setStatuses((s) => ({ ...s, [id]: "queued" }));
setQueue((curr) => [...curr, { id, downloadFn }]);
scheduleNext();
},
[scheduleNext]
);
const getStatus = useCallback((id) => statuses[id] || "idle", [statuses]);
return { startDownload, getStatus };
}
+324
View File
@@ -0,0 +1,324 @@
// // import { useState } from "react";
// // export default function DocumentLink({ detailsObj, caseReference }) {
// // const [downloadStatus, setDownloadStatus] = useState("");
// // const [progress, setProgress] = useState(null);
// // const handleDownload = async () => {
// // setDownloadStatus("Downloading");
// // setProgress(0);
// // try {
// // const res = await fetch(detailsObj.pinswg_hashlink);
// // if (!res.ok) throw new Error("Download failed");
// // const contentDisposition = res.headers.get("content-disposition");
// // const filename = contentDisposition
// // ? contentDisposition.split("filename=")[1]
// // : detailsObj.pinswg_name || "document";
// // const contentLength = res.headers.get("content-length");
// // const totalBytes = contentLength
// // ? parseInt(contentLength, 10)
// // : null;
// // const reader = res.body.getReader();
// // let receivedBytes = 0;
// // const chunks = [];
// // while (true) {
// // const { done, value } = await reader.read();
// // if (done) break;
// // chunks.push(value);
// // receivedBytes += value.length;
// // if (totalBytes) {
// // const percent = Math.round(
// // (receivedBytes / totalBytes) * 100
// // );
// // setProgress(percent);
// // }
// // }
// // // Combine chunks into one blob
// // const blob = new Blob(chunks);
// // const url = window.URL.createObjectURL(blob);
// // const link = document.createElement("a");
// // link.href = url;
// // link.download = filename;
// // document.body.appendChild(link);
// // link.click();
// // document.body.removeChild(link);
// // setDownloadStatus("Download complete!");
// // setProgress(100);
// // // Fire GA event
// // window.gtag?.("event", "DownloadedFile", {
// // caseReference,
// // filename,
// // });
// // } catch (err) {
// // console.error(err);
// // setDownloadStatus("Download failed");
// // setProgress(null);
// // }
// // };
// // return (
// // <>
// // {detailsObj.pinswg_publishtoweb ? (
// // <button className="documentLink" onClick={handleDownload}>
// // <span className="results-visually-hidden">
// // Document name:
// // </span>
// // {detailsObj.pinswg_name || ""}
// // </button>
// // ) : (
// // <>
// // <span className="results-visually-hidden">
// // Document name:
// // </span>
// // {detailsObj.pinswg_name || ""}
// // </>
// // )}
// // {downloadStatus && (
// // <p
// // className={
// // downloadStatus == "Downloading"
// // ? "govuk-!-font-size-14 govuk-!-margin-left-0 textloading"
// // : "govuk-!-font-size-14 govuk-!-margin-left-0 "
// // }
// // >
// // {downloadStatus}
// // {/* {progress !== null && ` (${progress}%)`} */}
// // </p>
// // )}
// // </>
// // );
// // }
// import { useState } from "react";
// export default function DocumentLink({
// id,
// detailsObj,
// caseReference,
// startDownload,
// getStatus,
// }) {
// const [downloadStatus, setDownloadStatus] = useState("");
// const [progress, setProgress] = useState(null);
// const downloadFile = async () => {
// setDownloadStatus("Downloading");
// setProgress(0);
// try {
// const res = await fetch(detailsObj.pinswg_hashlink);
// if (!res.ok) throw new Error("Download failed");
// const contentDisposition = res.headers.get("content-disposition");
// const filename = contentDisposition
// ? contentDisposition.split("filename=")[1]
// : detailsObj.pinswg_name || "document";
// const contentLength = res.headers.get("content-length");
// const totalBytes = contentLength
// ? parseInt(contentLength, 10)
// : null;
// const reader = res.body.getReader();
// let receivedBytes = 0;
// const chunks = [];
// while (true) {
// const { done, value } = await reader.read();
// if (done) break;
// chunks.push(value);
// receivedBytes += value.length;
// if (totalBytes) {
// const percent = Math.round(
// (receivedBytes / totalBytes) * 100
// );
// setProgress(percent);
// }
// }
// const blob = new Blob(chunks);
// const url = window.URL.createObjectURL(blob);
// const link = document.createElement("a");
// link.href = url;
// link.download = filename;
// document.body.appendChild(link);
// link.click();
// document.body.removeChild(link);
// setDownloadStatus("Download complete!");
// setProgress(100);
// window.gtag?.("event", "DownloadedFile", {
// caseReference,
// filename,
// });
// } catch (err) {
// console.error(err);
// setDownloadStatus("Download failed");
// setProgress(null);
// }
// };
// const handleClick = () => {
// if (getStatus(id) !== "idle") return; // don't double enqueue
// startDownload(id, downloadFile);
// };
// const status = getStatus(id);
// return (
// <>
// {detailsObj.pinswg_publishtoweb ? (
// <button className="documentLink" onClick={handleClick}>
// <span className="results-visually-hidden">
// Document name:
// </span>
// {detailsObj.pinswg_name || ""}
// </button>
// ) : (
// <>
// <span className="results-visually-hidden">
// Document name:
// </span>
// {detailsObj.pinswg_name || ""}
// </>
// )}
// {status !== "idle" && (
// <p
// className={
// status === "downloading"
// ? "govuk-!-font-size-14 govuk-!-margin-left-0 textloading"
// : "govuk-!-font-size-14 govuk-!-margin-left-0"
// }
// >
// {status === "downloading" && progress !== null
// ? `Downloading (${progress}%)`
// : status === "queued"
// ? "Queued"
// : status === "done"
// ? "Download complete!"
// : status === "failed"
// ? "Download failed"
// : ""}
// </p>
// )}
// </>
// );
// }
import { useState } from "react";
export default function DocumentLink({
id,
detailsObj,
caseReference,
startDownload,
getStatus,
}) {
const [progress, setProgress] = useState(null);
const downloadFile = async () => {
setProgress(0);
const res = await fetch(detailsObj.pinswg_hashlink);
if (!res.ok) throw new Error("Download failed");
const contentDisposition = res.headers.get("content-disposition");
const filename = contentDisposition
? contentDisposition.split("filename=")[1]
: detailsObj.pinswg_name || "document";
const contentLength = res.headers.get("content-length");
const totalBytes = contentLength ? parseInt(contentLength, 10) : null;
const reader = res.body.getReader();
let receivedBytes = 0;
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
receivedBytes += value.length;
if (totalBytes) {
const percent = Math.round((receivedBytes / totalBytes) * 100);
setProgress(percent);
}
}
const blob = new Blob(chunks);
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
setProgress(100);
window.gtag?.("event", "DownloadedFile", { caseReference, filename });
};
const handleClick = () => {
if (getStatus(id) !== "idle") return; // avoid double enqueue
startDownload(id, downloadFile);
};
const status = getStatus(id);
return (
<div>
{detailsObj.pinswg_publishtoweb ? (
<button className="documentLink" onClick={handleClick}>
<span className="results-visually-hidden">
Document name:
</span>
{detailsObj.pinswg_name || ""}
</button>
) : (
<span className="results-visually-hidden">
{detailsObj.pinswg_name || ""}
</span>
)}
{status !== "idle" && (
<p
className={
status === "downloading"
? "govuk-!-font-size-14 govuk-!-margin-left-0 textloading"
: "govuk-!-font-size-14 govuk-!-margin-left-0"
}
>
{status === "downloading" && progress !== null
? `Downloading `
: status === "queued"
? "Queued"
: status === "done"
? "Download complete!"
: status === "failed"
? "Download failed"
: ""}
</p>
)}
</div>
);
}