fix: abort ongoing requests when changing pages (#3927)

This commit is contained in:
manx98 2025-06-29 15:38:03 +08:00 committed by GitHub
parent 2d1a82b73f
commit 93c4b2e03c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 75 additions and 24 deletions

View File

@ -2,14 +2,22 @@ import { useAuthStore } from "@/stores/auth";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import { baseURL } from "@/utils/constants"; import { baseURL } from "@/utils/constants";
import { upload as postTus, useTus } from "./tus"; import { upload as postTus, useTus } from "./tus";
import { createURL, fetchURL, removePrefix } from "./utils"; import { createURL, fetchURL, removePrefix, StatusError } from "./utils";
export async function fetch(url: string) { export async function fetch(url: string, signal?: AbortSignal) {
url = removePrefix(url); url = removePrefix(url);
const res = await fetchURL(`/api/resources${url}`, { signal });
const res = await fetchURL(`/api/resources${url}`, {}); let data: Resource;
try {
const data = (await res.json()) as Resource; data = (await res.json()) as Resource;
} catch (e) {
// Check if the error is an intentional cancellation
if (e instanceof Error && e.name === "AbortError") {
throw new StatusError("000 No connection", 0, true);
}
throw e;
}
data.url = `/files${url}`; data.url = `/files${url}`;
if (data.isDir) { if (data.isDir) {
@ -205,10 +213,18 @@ export function getSubtitlesURL(file: ResourceItem) {
return file.subtitles?.map((d) => createURL("api/subtitle" + d, params)); return file.subtitles?.map((d) => createURL("api/subtitle" + d, params));
} }
export async function usage(url: string) { export async function usage(url: string, signal: AbortSignal) {
url = removePrefix(url); url = removePrefix(url);
const res = await fetchURL(`/api/usage${url}`, {}); const res = await fetchURL(`/api/usage${url}`, { signal });
return await res.json(); try {
return await res.json();
} catch (e) {
// Check if the error is an intentional cancellation
if (e instanceof Error && e.name == "AbortError") {
throw new StatusError("000 No connection", 0, true);
}
throw e;
}
} }

View File

@ -6,7 +6,8 @@ import { encodePath } from "@/utils/url";
export class StatusError extends Error { export class StatusError extends Error {
constructor( constructor(
message: any, message: any,
public status?: number public status?: number,
public is_canceled?: boolean
) { ) {
super(message); super(message);
this.name = "StatusError"; this.name = "StatusError";
@ -33,7 +34,11 @@ export async function fetchURL(
}, },
...rest, ...rest,
}); });
} catch { } catch (e) {
// Check if the error is an intentional cancellation
if (e instanceof Error && e.name === "AbortError") {
throw new StatusError("000 No connection", 0, true);
}
throw new StatusError("000 No connection", 0); throw new StatusError("000 No connection", 0);
} }

View File

@ -129,6 +129,7 @@ import {
import { files as api } from "@/api"; import { files as api } from "@/api";
import ProgressBar from "@/components/ProgressBar.vue"; import ProgressBar from "@/components/ProgressBar.vue";
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";
import { StatusError } from "@/api/utils.js";
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 }; const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
@ -136,7 +137,7 @@ export default {
name: "sidebar", name: "sidebar",
setup() { setup() {
const usage = reactive(USAGE_DEFAULT); const usage = reactive(USAGE_DEFAULT);
return { usage }; return { usage, usageAbortController: new AbortController() };
}, },
components: { components: {
ProgressBar, ProgressBar,
@ -157,6 +158,9 @@ export default {
}, },
methods: { methods: {
...mapActions(useLayoutStore, ["closeHovers", "showHover"]), ...mapActions(useLayoutStore, ["closeHovers", "showHover"]),
abortOngoingFetchUsage() {
this.usageAbortController.abort();
},
async fetchUsage() { async fetchUsage() {
const path = this.$route.path.endsWith("/") const path = this.$route.path.endsWith("/")
? this.$route.path ? this.$route.path
@ -166,13 +170,18 @@ export default {
return Object.assign(this.usage, usageStats); return Object.assign(this.usage, usageStats);
} }
try { try {
const usage = await api.usage(path); this.abortOngoingFetchUsage();
this.usageAbortController = new AbortController();
const usage = await api.usage(path, this.usageAbortController.signal);
usageStats = { usageStats = {
used: prettyBytes(usage.used, { binary: true }), used: prettyBytes(usage.used, { binary: true }),
total: prettyBytes(usage.total, { binary: true }), total: prettyBytes(usage.total, { binary: true }),
usedPercentage: Math.round((usage.used / usage.total) * 100), usedPercentage: Math.round((usage.used / usage.total) * 100),
}; };
} catch (error) { } catch (error) {
if (error instanceof StatusError && error.is_canceled) {
return;
}
this.$showError(error); this.$showError(error);
} }
return Object.assign(this.usage, usageStats); return Object.assign(this.usage, usageStats);
@ -200,5 +209,8 @@ export default {
immediate: true, immediate: true,
}, },
}, },
unmounted() {
this.abortOngoingFetchUsage();
},
}; };
</script> </script>

View File

@ -31,6 +31,7 @@ import { useFileStore } from "@/stores/file";
import url from "@/utils/url"; import url from "@/utils/url";
import { files } from "@/api"; import { files } from "@/api";
import { StatusError } from "@/api/utils.js";
export default { export default {
name: "file-list", name: "file-list",
@ -43,6 +44,7 @@ export default {
}, },
selected: null, selected: null,
current: window.location.pathname, current: window.location.pathname,
nextAbortController: new AbortController(),
}; };
}, },
inject: ["$showError"], inject: ["$showError"],
@ -56,7 +58,13 @@ export default {
mounted() { mounted() {
this.fillOptions(this.req); this.fillOptions(this.req);
}, },
unmounted() {
this.abortOngoingNext();
},
methods: { methods: {
abortOngoingNext() {
this.nextAbortController.abort();
},
fillOptions(req) { fillOptions(req) {
// Sets the current path and resets // Sets the current path and resets
// the current items. // the current items.
@ -94,8 +102,17 @@ export default {
// just clicked in and fill the options with its // just clicked in and fill the options with its
// content. // content.
const uri = event.currentTarget.dataset.url; const uri = event.currentTarget.dataset.url;
this.abortOngoingNext();
files.fetch(uri).then(this.fillOptions).catch(this.$showError); this.nextAbortController = new AbortController();
files
.fetch(uri, this.nextAbortController.signal)
.then(this.fillOptions)
.catch((e) => {
if (e instanceof StatusError && e.is_canceled) {
return;
}
this.$showError(e);
});
}, },
touchstart(event) { touchstart(event) {
const url = event.currentTarget.dataset.url; const url = event.currentTarget.dataset.url;

View File

@ -10,6 +10,7 @@ interface ApiOpts {
method?: ApiMethod; method?: ApiMethod;
headers?: object; headers?: object;
body?: any; body?: any;
signal?: AbortSignal;
} }
interface TusSettings { interface TusSettings {

View File

@ -61,9 +61,7 @@ const route = useRoute();
const { t } = useI18n({}); const { t } = useI18n({});
const clean = (path: string) => { let fetchDataController = new AbortController();
return path.endsWith("/") ? path.slice(0, -1) : path;
};
const error = ref<StatusError | null>(null); const error = ref<StatusError | null>(null);
@ -101,6 +99,7 @@ onUnmounted(() => {
layoutStore.toggleShell(); layoutStore.toggleShell();
} }
fileStore.updateRequest(null); fileStore.updateRequest(null);
fetchDataController.abort();
}); });
watch(route, (to, from) => { watch(route, (to, from) => {
@ -142,20 +141,21 @@ const fetchData = async () => {
let url = route.path; let url = route.path;
if (url === "") url = "/"; if (url === "") url = "/";
if (url[0] !== "/") url = "/" + url; if (url[0] !== "/") url = "/" + url;
// Cancel the ongoing request
fetchDataController.abort();
fetchDataController = new AbortController();
try { try {
const res = await api.fetch(url); const res = await api.fetch(url, fetchDataController.signal);
if (clean(res.path) !== clean(`/${[...route.params.path].join("/")}`)) {
throw new Error("Data Mismatch!");
}
fileStore.updateRequest(res); fileStore.updateRequest(res);
document.title = `${res.name || t("sidebar.myFiles")} - ${t("files.files")} - ${name}`; document.title = `${res.name || t("sidebar.myFiles")} - ${t("files.files")} - ${name}`;
layoutStore.loading = false;
} catch (err) { } catch (err) {
if (err instanceof StatusError && err.is_canceled) {
return;
}
if (err instanceof Error) { if (err instanceof Error) {
error.value = err; error.value = err;
} }
} finally {
layoutStore.loading = false; layoutStore.loading = false;
} }
}; };