From 9e8cb1c7557f0fa8762be1f73d53e3797f72302c Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Sat, 14 Feb 2026 10:04:29 +0100 Subject: [PATCH] refactor: improve request cancellation and timeout handling --- apps/client/src/App.tsx | 3 ++ apps/client/src/common/api/assets.ts | 9 ++-- apps/client/src/common/api/automation.ts | 5 ++- apps/client/src/common/api/axios.config.ts | 5 ++- apps/client/src/common/api/customFields.ts | 5 ++- apps/client/src/common/api/db.ts | 12 ++++-- apps/client/src/common/api/excel.ts | 28 ++++++++++--- apps/client/src/common/api/external.ts | 6 ++- apps/client/src/common/api/project.ts | 10 ++++- apps/client/src/common/api/report.ts | 5 ++- apps/client/src/common/api/requestOptions.ts | 4 ++ apps/client/src/common/api/requestTimeouts.ts | 8 ++++ apps/client/src/common/api/rundown.ts | 9 ++-- apps/client/src/common/api/session.ts | 5 ++- apps/client/src/common/api/settings.ts | 5 ++- apps/client/src/common/api/sheets.ts | 41 ++++++++++++++++--- apps/client/src/common/api/urlPresets.ts | 5 ++- apps/client/src/common/api/viewSettings.ts | 5 ++- .../src/common/hooks-query/useAppVersion.ts | 2 +- .../hooks-query/useAutomationSettings.ts | 5 +-- .../src/common/hooks-query/useCustomFields.ts | 5 +-- .../hooks-query/useCustomTranslation.ts | 2 +- apps/client/src/common/hooks-query/useInfo.ts | 5 +-- .../src/common/hooks-query/useProjectData.ts | 2 +- .../src/common/hooks-query/useProjectList.ts | 5 +-- .../common/hooks-query/useProjectRundowns.ts | 2 +- .../src/common/hooks-query/useReport.ts | 5 +-- .../src/common/hooks-query/useRundown.ts | 2 +- .../src/common/hooks-query/useSettings.ts | 2 +- .../src/common/hooks-query/useUrlPresets.ts | 2 +- .../src/common/hooks-query/useViewSettings.ts | 2 +- apps/client/src/common/queryClient.ts | 13 ++++-- 32 files changed, 152 insertions(+), 72 deletions(-) create mode 100644 apps/client/src/common/api/requestOptions.ts create mode 100644 apps/client/src/common/api/requestTimeouts.ts diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx index f8cdf1d7f..a01e4bce8 100644 --- a/apps/client/src/App.tsx +++ b/apps/client/src/App.tsx @@ -3,6 +3,9 @@ import { Tooltip } from '@base-ui/react/tooltip'; import { QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +// apply global axios config defaults +import './common/api/axios.config'; + import ErrorBoundary from './common/components/error-boundary/ErrorBoundary'; import IdentifyOverlay from './common/components/identify-overlay/IdentifyOverlay'; import { AppContextProvider } from './common/context/AppContext'; diff --git a/apps/client/src/common/api/assets.ts b/apps/client/src/common/api/assets.ts index 65be05f78..f853eb5b8 100644 --- a/apps/client/src/common/api/assets.ts +++ b/apps/client/src/common/api/assets.ts @@ -4,14 +4,15 @@ import { TranslationObject } from 'ontime-types'; import { ontimeQueryClient } from '../../common/queryClient'; import { apiEntryUrl, customTranslationsURL, TRANSLATION } from './constants'; +import type { RequestOptions } from './requestOptions'; const assetsPath = `${apiEntryUrl}/assets`; /** * HTTP request to get css contents */ -export async function getCSSContents(): Promise { - const res = await axios.get(`${assetsPath}/css`); +export async function getCSSContents(options?: RequestOptions): Promise { + const res = await axios.get(`${assetsPath}/css`, { signal: options?.signal }); return res.data; } @@ -35,8 +36,8 @@ export async function restoreCSSContents(): Promise { /** * HTTP request to get user translation */ -export async function getUserTranslation(): Promise { - const res = await axios.get(customTranslationsURL); +export async function getUserTranslation(options?: RequestOptions): Promise { + const res = await axios.get(customTranslationsURL, { signal: options?.signal }); return res.data; } diff --git a/apps/client/src/common/api/automation.ts b/apps/client/src/common/api/automation.ts index e6c031989..5f37fa27a 100644 --- a/apps/client/src/common/api/automation.ts +++ b/apps/client/src/common/api/automation.ts @@ -9,14 +9,15 @@ import type { } from 'ontime-types'; import { apiEntryUrl } from './constants'; +import type { RequestOptions } from './requestOptions'; const automationsPath = `${apiEntryUrl}/automations`; /** * HTTP request to get the automations settings */ -export async function getAutomationSettings(): Promise { - const res = await axios.get(automationsPath); +export async function getAutomationSettings(options?: RequestOptions): Promise { + const res = await axios.get(automationsPath, { signal: options?.signal }); return res.data; } diff --git a/apps/client/src/common/api/axios.config.ts b/apps/client/src/common/api/axios.config.ts index 9d993bc85..5ae9a50b8 100644 --- a/apps/client/src/common/api/axios.config.ts +++ b/apps/client/src/common/api/axios.config.ts @@ -1,5 +1,8 @@ import axios from 'axios'; +import { axiosConfig } from './requestTimeouts'; + axios.defaults.validateStatus = (status) => { - return (status >= 200 && status < 300) || status === 304; + return status >= 200 && status < 300; }; +axios.defaults.timeout = axiosConfig.shortTimeout; diff --git a/apps/client/src/common/api/customFields.ts b/apps/client/src/common/api/customFields.ts index 02ce4d15e..1a5b136e1 100644 --- a/apps/client/src/common/api/customFields.ts +++ b/apps/client/src/common/api/customFields.ts @@ -2,14 +2,15 @@ import axios from 'axios'; import { CustomField, CustomFieldKey, CustomFields } from 'ontime-types'; import { apiEntryUrl } from './constants'; +import type { RequestOptions } from './requestOptions'; const customFieldsPath = `${apiEntryUrl}/custom-fields`; /** * Requests list of known custom fields */ -export async function getCustomFields(): Promise { - const res = await axios.get(customFieldsPath); +export async function getCustomFields(options?: RequestOptions): Promise { + const res = await axios.get(customFieldsPath, { signal: options?.signal }); return res.data; } diff --git a/apps/client/src/common/api/db.ts b/apps/client/src/common/api/db.ts index 28f3b9b33..cae17c3fa 100644 --- a/apps/client/src/common/api/db.ts +++ b/apps/client/src/common/api/db.ts @@ -2,6 +2,8 @@ import axios, { AxiosResponse } from 'axios'; import { DatabaseModel, MessageResponse, ProjectData, ProjectFileListResponse, QuickStartData } from 'ontime-types'; import { apiEntryUrl } from './constants'; +import type { RequestOptions } from './requestOptions'; +import { axiosConfig } from './requestTimeouts'; import { createBlob, downloadBlob } from './utils'; const dbPath = `${apiEntryUrl}/db`; @@ -10,7 +12,7 @@ const dbPath = `${apiEntryUrl}/db`; * HTTP request to the current DB */ export function getDb(filename: string): Promise> { - return axios.post(`${dbPath}/download`, { filename }); + return axios.post(`${dbPath}/download`, { filename }, { timeout: axiosConfig.longTimeout }); } /** @@ -37,6 +39,7 @@ export async function uploadProjectFile(file: File): Promise { const formData = new FormData(); formData.append('project', file); const response = await axios.post(`${dbPath}/upload`, formData, { + timeout: axiosConfig.longTimeout, headers: { 'Content-Type': 'multipart/form-data', }, @@ -76,8 +79,11 @@ export async function quickProject(data: QuickStartData): Promise { - const res = await axios.get(`${dbPath}/all`); +export async function getProjects(options?: RequestOptions): Promise { + const res = await axios.get(`${dbPath}/all`, { + signal: options?.signal, + timeout: options?.timeout, + }); return res.data; } diff --git a/apps/client/src/common/api/excel.ts b/apps/client/src/common/api/excel.ts index a25275be7..38d78d50c 100644 --- a/apps/client/src/common/api/excel.ts +++ b/apps/client/src/common/api/excel.ts @@ -3,6 +3,8 @@ import { CustomFields, Rundown, RundownSummary } from 'ontime-types'; import { ImportMap } from 'ontime-utils'; import { apiEntryUrl } from './constants'; +import type { RequestOptions } from './requestOptions'; +import { axiosConfig } from './requestTimeouts'; import { downloadBlob } from './utils'; const excelPath = `${apiEntryUrl}/excel`; @@ -11,10 +13,12 @@ const excelPath = `${apiEntryUrl}/excel`; * upload Excel file to server * @return string - file ID op the uploaded file */ -export async function upload(file: File): Promise { +export async function upload(file: File, requestOptions?: RequestOptions): Promise { const formData = new FormData(); formData.append('excel', file); const response = await axios.post(`${excelPath}/upload`, formData, { + signal: requestOptions?.signal, + timeout: requestOptions?.timeout ?? axiosConfig.longTimeout, headers: { 'Content-Type': 'multipart/form-data', }, @@ -27,19 +31,31 @@ type PreviewSpreadsheetResponse = { customFields: CustomFields; summary: RundownSummary; }; -export async function importRundownPreview(options: ImportMap): Promise { - const response: AxiosResponse = await axios.post(`${excelPath}/preview`, { - options, - }); +export async function importRundownPreview( + options: ImportMap, + requestOptions?: RequestOptions, +): Promise { + const response: AxiosResponse = await axios.post( + `${excelPath}/preview`, + { + options, + }, + { + signal: requestOptions?.signal, + timeout: requestOptions?.timeout ?? axiosConfig.longTimeout, + }, + ); return response.data; } /** * Downloads a xlsx representation of the rundown from the server */ -export async function downloadAsExcel(rundownId: string, fileName?: string) { +export async function downloadAsExcel(rundownId: string, fileName?: string, requestOptions?: RequestOptions) { try { const response = await axios.get(`${excelPath}/${rundownId}/export`, { + signal: requestOptions?.signal, + timeout: requestOptions?.timeout ?? axiosConfig.longTimeout, responseType: 'blob', }); diff --git a/apps/client/src/common/api/external.ts b/apps/client/src/common/api/external.ts index 9c2c0ba53..7156b4609 100644 --- a/apps/client/src/common/api/external.ts +++ b/apps/client/src/common/api/external.ts @@ -2,6 +2,8 @@ import axios from 'axios'; import { apiRepoLatest } from '../../externals'; +import type { RequestOptions } from './requestOptions'; + export type HasUpdate = { url: string; version: string; @@ -10,8 +12,8 @@ export type HasUpdate = { /** * HTTP request to get the latest version and url from github */ -export async function getLatestVersion(): Promise { - const res = await axios.get(apiRepoLatest); +export async function getLatestVersion(options?: RequestOptions): Promise { + const res = await axios.get(apiRepoLatest, { signal: options?.signal }); return { url: res.data.html_url as string, version: res.data.tag_name as string, diff --git a/apps/client/src/common/api/project.ts b/apps/client/src/common/api/project.ts index 50e7689d6..1fd1cf52f 100644 --- a/apps/client/src/common/api/project.ts +++ b/apps/client/src/common/api/project.ts @@ -2,14 +2,19 @@ import axios, { AxiosResponse } from 'axios'; import { ProjectData, ProjectLogoResponse } from 'ontime-types'; import { apiEntryUrl } from './constants'; +import type { RequestOptions } from './requestOptions'; +import { axiosConfig } from './requestTimeouts'; const projectPath = `${apiEntryUrl}/project`; /** * HTTP request to fetch project data */ -export async function getProjectData(): Promise { - const res = await axios.get(projectPath); +export async function getProjectData(options?: RequestOptions): Promise { + const res = await axios.get(projectPath, { + signal: options?.signal, + timeout: options?.timeout, + }); return res.data; } @@ -27,6 +32,7 @@ export async function uploadProjectLogo(file: File): Promise { - const res = await axios.get(reportUrl); +export async function fetchReport(options?: RequestOptions): Promise { + const res = await axios.get(reportUrl, { signal: options?.signal }); return res.data; } diff --git a/apps/client/src/common/api/requestOptions.ts b/apps/client/src/common/api/requestOptions.ts new file mode 100644 index 000000000..636ef26e0 --- /dev/null +++ b/apps/client/src/common/api/requestOptions.ts @@ -0,0 +1,4 @@ +export type RequestOptions = { + signal?: AbortSignal; + timeout?: number; +}; diff --git a/apps/client/src/common/api/requestTimeouts.ts b/apps/client/src/common/api/requestTimeouts.ts new file mode 100644 index 000000000..d83cdc407 --- /dev/null +++ b/apps/client/src/common/api/requestTimeouts.ts @@ -0,0 +1,8 @@ +/** + * Keep a short global timeout for regular API requests, but allow + * longer windows for file transfer and heavy import/export operations. + */ +export const axiosConfig = { + shortTimeout: 20 * 1000, // 20 seconds + longTimeout: 3 * 60 * 1000, // 3 minutes +} as const; diff --git a/apps/client/src/common/api/rundown.ts b/apps/client/src/common/api/rundown.ts index 42852d1fa..e2cebb889 100644 --- a/apps/client/src/common/api/rundown.ts +++ b/apps/client/src/common/api/rundown.ts @@ -2,6 +2,7 @@ import axios, { AxiosResponse } from 'axios'; import { EntryId, OntimeEntry, OntimeEvent, ProjectRundownsList, Rundown, TransientEventPayload } from 'ontime-types'; import { apiEntryUrl } from './constants'; +import type { RequestOptions } from './requestOptions'; type RundownId = string; const rundownPath = `${apiEntryUrl}/rundowns`; @@ -11,16 +12,16 @@ const rundownPath = `${apiEntryUrl}/rundowns`; /** * HTTP request to fetch a list of existing rundowns */ -export async function fetchProjectRundownList(): Promise { - const res = await axios.get(rundownPath); +export async function fetchProjectRundownList(options?: RequestOptions): Promise { + const res = await axios.get(rundownPath, { signal: options?.signal }); return res.data; } /** * HTTP request to fetch all entries in the currently loaded rundown */ -export async function fetchCurrentRundown(): Promise { - const res = await axios.get(`${rundownPath}/current`); +export async function fetchCurrentRundown(options?: RequestOptions): Promise { + const res = await axios.get(`${rundownPath}/current`, { signal: options?.signal }); if (!isValidRundown(res.data)) { throw new Error('Invalid rundown payload'); } diff --git a/apps/client/src/common/api/session.ts b/apps/client/src/common/api/session.ts index ae71fa512..1fe55ccef 100644 --- a/apps/client/src/common/api/session.ts +++ b/apps/client/src/common/api/session.ts @@ -2,14 +2,15 @@ import axios from 'axios'; import { GetInfo, LinkOptions } from 'ontime-types'; import { apiEntryUrl } from './constants'; +import type { RequestOptions } from './requestOptions'; const sessionPath = `${apiEntryUrl}/session`; /** * HTTP request to retrieve application info */ -export async function getInfo(): Promise { - const res = await axios.get(`${sessionPath}/info`); +export async function getInfo(options?: RequestOptions): Promise { + const res = await axios.get(`${sessionPath}/info`, { signal: options?.signal }); return res.data; } diff --git a/apps/client/src/common/api/settings.ts b/apps/client/src/common/api/settings.ts index 29b6ab469..da37683d1 100644 --- a/apps/client/src/common/api/settings.ts +++ b/apps/client/src/common/api/settings.ts @@ -2,14 +2,15 @@ import axios, { AxiosResponse } from 'axios'; import { Settings } from 'ontime-types'; import { apiEntryUrl } from './constants'; +import type { RequestOptions } from './requestOptions'; const settingsPath = `${apiEntryUrl}/settings`; /** * HTTP request to retrieve application settings */ -export async function getSettings(): Promise { - const res = await axios.get(settingsPath); +export async function getSettings(options?: RequestOptions): Promise { + const res = await axios.get(settingsPath, { signal: options?.signal }); return res.data; } diff --git a/apps/client/src/common/api/sheets.ts b/apps/client/src/common/api/sheets.ts index fa070b8f5..27fefa329 100644 --- a/apps/client/src/common/api/sheets.ts +++ b/apps/client/src/common/api/sheets.ts @@ -3,6 +3,8 @@ import { AuthenticationStatus, CustomFields, Rundown, RundownSummary } from 'ont import { ImportMap } from 'ontime-utils'; import { apiEntryUrl } from './constants'; +import type { RequestOptions } from './requestOptions'; +import { axiosConfig } from './requestTimeouts'; const sheetsPath = `${apiEntryUrl}/sheets`; @@ -23,6 +25,7 @@ export const verifyAuthenticationStatus = async (): Promise<{ export const requestConnection = async ( file: File, sheetId: string, + requestOptions?: RequestOptions, ): Promise<{ verification_url: string; user_code: string; @@ -31,6 +34,8 @@ export const requestConnection = async ( formData.append('client_secret', file); const response = await axios.post(`${sheetsPath}/${sheetId}/connect`, formData, { + signal: requestOptions?.signal, + timeout: requestOptions?.timeout ?? axiosConfig.longTimeout, headers: { 'Content-Type': 'multipart/form-data', }, @@ -53,24 +58,50 @@ export const revokeAuthentication = async (): Promise<{ authenticated: Authentic export const previewRundown = async ( sheetId: string, options: ImportMap, + requestOptions?: RequestOptions, ): Promise<{ rundown: Rundown; customFields: CustomFields; summary: RundownSummary; }> => { - const response = await axios.post(`${sheetsPath}/${sheetId}/read`, { options }); + const response = await axios.post( + `${sheetsPath}/${sheetId}/read`, + { options }, + { + signal: requestOptions?.signal, + timeout: requestOptions?.timeout ?? axiosConfig.longTimeout, + }, + ); return response.data; }; -export const getWorksheetNames = async (sheetId: string): Promise => { - const response: AxiosResponse = await axios.post(`${sheetsPath}/${sheetId}/worksheets`); +export const getWorksheetNames = async (sheetId: string, requestOptions?: RequestOptions): Promise => { + const response: AxiosResponse = await axios.post( + `${sheetsPath}/${sheetId}/worksheets`, + undefined, + { + signal: requestOptions?.signal, + timeout: requestOptions?.timeout ?? axiosConfig.longTimeout, + }, + ); return response.data; }; /** * HTTP request to upload the rundown to a google sheet */ -export const uploadRundown = async (sheetId: string, options: ImportMap): Promise => { - const response = await axios.post(`${sheetsPath}/${sheetId}/write`, { options }); +export const uploadRundown = async ( + sheetId: string, + options: ImportMap, + requestOptions?: RequestOptions, +): Promise => { + const response = await axios.post( + `${sheetsPath}/${sheetId}/write`, + { options }, + { + signal: requestOptions?.signal, + timeout: requestOptions?.timeout ?? axiosConfig.longTimeout, + }, + ); return response.data; }; diff --git a/apps/client/src/common/api/urlPresets.ts b/apps/client/src/common/api/urlPresets.ts index 454a80c67..738f8073f 100644 --- a/apps/client/src/common/api/urlPresets.ts +++ b/apps/client/src/common/api/urlPresets.ts @@ -2,14 +2,15 @@ import axios from 'axios'; import { URLPreset } from 'ontime-types'; import { apiEntryUrl } from './constants'; +import type { RequestOptions } from './requestOptions'; const urlPresetsPath = `${apiEntryUrl}/url-presets`; /** * HTTP request to retrieve all presets */ -export async function getUrlPresets(): Promise { - const res = await axios.get(urlPresetsPath); +export async function getUrlPresets(options?: RequestOptions): Promise { + const res = await axios.get(urlPresetsPath, { signal: options?.signal }); return res.data; } diff --git a/apps/client/src/common/api/viewSettings.ts b/apps/client/src/common/api/viewSettings.ts index 9eb87505f..fcf85c45b 100644 --- a/apps/client/src/common/api/viewSettings.ts +++ b/apps/client/src/common/api/viewSettings.ts @@ -2,6 +2,7 @@ import axios from 'axios'; import type { ViewSettings } from 'ontime-types'; import { apiEntryUrl } from './constants'; +import type { RequestOptions } from './requestOptions'; const viewSettingsPath = `${apiEntryUrl}/view-settings`; @@ -9,8 +10,8 @@ const viewSettingsPath = `${apiEntryUrl}/view-settings`; * HTTP request to fetch view settings * @returns */ -export async function getViewSettings(): Promise { - const res = await axios.get(viewSettingsPath); +export async function getViewSettings(options?: RequestOptions): Promise { + const res = await axios.get(viewSettingsPath, { signal: options?.signal }); return res.data; } diff --git a/apps/client/src/common/hooks-query/useAppVersion.ts b/apps/client/src/common/hooks-query/useAppVersion.ts index 399380b8d..84bcc16fa 100644 --- a/apps/client/src/common/hooks-query/useAppVersion.ts +++ b/apps/client/src/common/hooks-query/useAppVersion.ts @@ -17,7 +17,7 @@ export default function useAppVersion() { refetch, } = useQuery({ queryKey: APP_VERSION, - queryFn: getLatestVersion, + queryFn: ({ signal }) => getLatestVersion({ signal }), placeholderData: (previousData, _previousQuery) => previousData, refetchOnWindowFocus: false, refetchOnReconnect: false, diff --git a/apps/client/src/common/hooks-query/useAutomationSettings.ts b/apps/client/src/common/hooks-query/useAutomationSettings.ts index 18d9f81ab..76cbf9135 100644 --- a/apps/client/src/common/hooks-query/useAutomationSettings.ts +++ b/apps/client/src/common/hooks-query/useAutomationSettings.ts @@ -8,12 +8,9 @@ import { automationPlaceholderSettings } from '../models/AutomationSettings'; export default function useAutomationSettings() { const { data, status, isFetching, isError, refetch } = useQuery({ queryKey: AUTOMATION, - queryFn: getAutomationSettings, + queryFn: ({ signal }) => getAutomationSettings({ signal }), placeholderData: (previousData, _previousQuery) => previousData, - retry: 5, - retryDelay: (attempt: number) => attempt * 2500, refetchInterval: queryRefetchIntervalSlow, - networkMode: 'always', }); return { data: data ?? automationPlaceholderSettings, status, isFetching, isError, refetch }; diff --git a/apps/client/src/common/hooks-query/useCustomFields.ts b/apps/client/src/common/hooks-query/useCustomFields.ts index 36a087fb9..15dde9194 100644 --- a/apps/client/src/common/hooks-query/useCustomFields.ts +++ b/apps/client/src/common/hooks-query/useCustomFields.ts @@ -10,12 +10,9 @@ const placeholder: CustomFields = {}; export default function useCustomFields() { const { data, status, isFetching, isError, refetch } = useQuery({ queryKey: CUSTOM_FIELDS, - queryFn: getCustomFields, + queryFn: ({ signal }) => getCustomFields({ signal }), placeholderData: (previousData, _previousQuery) => previousData, - retry: 5, - retryDelay: (attempt) => attempt * 2500, refetchInterval: queryRefetchIntervalSlow, - networkMode: 'always', }); return { data: data ?? placeholder, status, isFetching, isError, refetch }; diff --git a/apps/client/src/common/hooks-query/useCustomTranslation.ts b/apps/client/src/common/hooks-query/useCustomTranslation.ts index e04acb612..51a70b1e3 100644 --- a/apps/client/src/common/hooks-query/useCustomTranslation.ts +++ b/apps/client/src/common/hooks-query/useCustomTranslation.ts @@ -8,7 +8,7 @@ import { queryRefetchIntervalSlow } from '../../ontimeConfig'; export function useCustomTranslation() { const { data, status, refetch } = useQuery({ queryKey: TRANSLATION, - queryFn: getUserTranslation, + queryFn: ({ signal }) => getUserTranslation({ signal }), placeholderData: (previousData, _previousQuery) => previousData, refetchInterval: queryRefetchIntervalSlow, }); diff --git a/apps/client/src/common/hooks-query/useInfo.ts b/apps/client/src/common/hooks-query/useInfo.ts index 1f295fc7a..7443b1524 100644 --- a/apps/client/src/common/hooks-query/useInfo.ts +++ b/apps/client/src/common/hooks-query/useInfo.ts @@ -9,12 +9,9 @@ import { ontimePlaceholderInfo } from '../models/Info'; export default function useInfo() { const { data, status, isError, refetch, isFetching } = useQuery({ queryKey: APP_INFO, - queryFn: getInfo, + queryFn: ({ signal }) => getInfo({ signal }), placeholderData: (previousData, _previousQuery) => previousData, - retry: 5, - retryDelay: (attempt) => attempt * 2500, refetchInterval: queryRefetchIntervalSlow, - networkMode: 'always', }); return { data: data ?? ontimePlaceholderInfo, status, isError, refetch, isFetching }; diff --git a/apps/client/src/common/hooks-query/useProjectData.ts b/apps/client/src/common/hooks-query/useProjectData.ts index e530e581c..f806f1a17 100644 --- a/apps/client/src/common/hooks-query/useProjectData.ts +++ b/apps/client/src/common/hooks-query/useProjectData.ts @@ -8,7 +8,7 @@ import { projectDataPlaceholder } from '../models/ProjectData'; export default function useProjectData() { const { data, status, isFetching, isError, refetch } = useQuery({ queryKey: PROJECT_DATA, - queryFn: getProjectData, + queryFn: ({ signal }) => getProjectData({ signal }), placeholderData: (previousData, _previousQuery) => previousData, refetchInterval: queryRefetchIntervalSlow, }); diff --git a/apps/client/src/common/hooks-query/useProjectList.ts b/apps/client/src/common/hooks-query/useProjectList.ts index fe35fbc46..7a88f2cc0 100644 --- a/apps/client/src/common/hooks-query/useProjectList.ts +++ b/apps/client/src/common/hooks-query/useProjectList.ts @@ -14,12 +14,9 @@ const placeholderProjectList: ProjectFileListResponse = { function useProjectList() { const { data, status, refetch } = useQuery({ queryKey: PROJECT_LIST, - queryFn: getProjects, + queryFn: ({ signal }) => getProjects({ signal }), placeholderData: (previousData, _previousQuery) => previousData, - retry: 5, - retryDelay: (attempt: number) => attempt * 2500, refetchInterval: queryRefetchIntervalSlow, - networkMode: 'always', }); return { data: data ?? placeholderProjectList, status, refetch }; } diff --git a/apps/client/src/common/hooks-query/useProjectRundowns.ts b/apps/client/src/common/hooks-query/useProjectRundowns.ts index fda4a2616..608980106 100644 --- a/apps/client/src/common/hooks-query/useProjectRundowns.ts +++ b/apps/client/src/common/hooks-query/useProjectRundowns.ts @@ -11,7 +11,7 @@ import { createRundown, deleteRundown, duplicateRundown, fetchProjectRundownList export function useProjectRundowns() { const { data, status, isError, refetch, isFetching } = useQuery({ queryKey: PROJECT_RUNDOWNS, - queryFn: fetchProjectRundownList, + queryFn: ({ signal }) => fetchProjectRundownList({ signal }), placeholderData: (previousData, _previousQuery) => previousData, refetchInterval: queryRefetchIntervalSlow, }); diff --git a/apps/client/src/common/hooks-query/useReport.ts b/apps/client/src/common/hooks-query/useReport.ts index 90bb4bc15..9f4510a52 100644 --- a/apps/client/src/common/hooks-query/useReport.ts +++ b/apps/client/src/common/hooks-query/useReport.ts @@ -8,11 +8,8 @@ import { fetchReport } from '../api/report'; export default function useReport() { const { data, refetch } = useQuery({ queryKey: REPORT, - queryFn: fetchReport, + queryFn: ({ signal }) => fetchReport({ signal }), placeholderData: (previousData, _previousQuery) => previousData, - retry: 5, - retryDelay: (attempt) => attempt * 2500, - networkMode: 'always', staleTime: MILLIS_PER_HOUR, }); diff --git a/apps/client/src/common/hooks-query/useRundown.ts b/apps/client/src/common/hooks-query/useRundown.ts index b97d55161..b805edd6d 100644 --- a/apps/client/src/common/hooks-query/useRundown.ts +++ b/apps/client/src/common/hooks-query/useRundown.ts @@ -24,7 +24,7 @@ const cachedRundownPlaceholder: Rundown = { export default function useRundown() { const { data, status, isError, refetch, isFetching } = useQuery({ queryKey: RUNDOWN, - queryFn: fetchCurrentRundown, + queryFn: ({ signal }) => fetchCurrentRundown({ signal }), refetchInterval: queryRefetchIntervalSlow, }); diff --git a/apps/client/src/common/hooks-query/useSettings.ts b/apps/client/src/common/hooks-query/useSettings.ts index 3d1aa985f..e14676424 100644 --- a/apps/client/src/common/hooks-query/useSettings.ts +++ b/apps/client/src/common/hooks-query/useSettings.ts @@ -8,7 +8,7 @@ import { ontimePlaceholderSettings } from '../models/OntimeSettings'; export default function useSettings() { const { data, status, isFetching, isError, refetch } = useQuery({ queryKey: APP_SETTINGS, - queryFn: getSettings, + queryFn: ({ signal }) => getSettings({ signal }), placeholderData: (previousData, _previousQuery) => previousData, select: (data) => { const unobfuscated = { ...data }; diff --git a/apps/client/src/common/hooks-query/useUrlPresets.ts b/apps/client/src/common/hooks-query/useUrlPresets.ts index bc48aaa29..e9e6bde9c 100644 --- a/apps/client/src/common/hooks-query/useUrlPresets.ts +++ b/apps/client/src/common/hooks-query/useUrlPresets.ts @@ -13,7 +13,7 @@ interface FetchProps { export default function useUrlPresets({ skip = false }: FetchProps = {}) { const { data, status, isError, refetch } = useQuery({ queryKey: URL_PRESETS, - queryFn: getUrlPresets, + queryFn: ({ signal }) => getUrlPresets({ signal }), placeholderData: (previousData, _previousQuery) => previousData, refetchInterval: queryRefetchIntervalSlow, enabled: !skip, diff --git a/apps/client/src/common/hooks-query/useViewSettings.ts b/apps/client/src/common/hooks-query/useViewSettings.ts index 882b08482..dc3a3b621 100644 --- a/apps/client/src/common/hooks-query/useViewSettings.ts +++ b/apps/client/src/common/hooks-query/useViewSettings.ts @@ -9,7 +9,7 @@ import { viewsSettingsPlaceholder } from '../models/ViewSettings.type'; export default function useViewSettings() { const { data, status } = useQuery({ queryKey: VIEW_SETTINGS, - queryFn: getViewSettings, + queryFn: ({ signal }) => getViewSettings({ signal }), placeholderData: (previousData, _previousQuery) => previousData, staleTime: MILLIS_PER_HOUR, }); diff --git a/apps/client/src/common/queryClient.ts b/apps/client/src/common/queryClient.ts index 81970932f..7b0ab995c 100644 --- a/apps/client/src/common/queryClient.ts +++ b/apps/client/src/common/queryClient.ts @@ -1,4 +1,5 @@ import { QueryClient } from '@tanstack/react-query'; +import axios from 'axios'; import { MILLIS_PER_MINUTE } from 'ontime-utils'; import { isOntimeCloud } from '../externals'; @@ -8,10 +9,15 @@ export const ontimeQueryClient = new QueryClient({ queries: { gcTime: 10 * MILLIS_PER_MINUTE, // staleTime: MILLIS_PER_HOUR, //TODO: when all routes have implemented refetch signal from server, we can the assume that the data is not stale until we get the signal - networkMode: 'always', + networkMode: isOntimeCloud ? 'online' : 'always', refetchOnWindowFocus: false, - retry: 5, - retryDelay: (attempt) => attempt * 2500, + retry: (failureCount, error) => { + if (axios.isCancel(error)) { + return false; + } + return failureCount < 5; + }, + retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 5000), }, mutations: { /** @@ -21,6 +27,7 @@ export const ontimeQueryClient = new QueryClient({ * - use 'online' for clients that are connected to the cloud */ networkMode: isOntimeCloud ? 'online' : 'always', + retry: 0, }, }, });