queued downloads and stream not buffer of downloads
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user