Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 62 additions & 6 deletions src/components/landing-page/use-fetch-production-list.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import { useEffect, useState } from "react";
import { useEffect, useState, useRef } from "react";
import { useGlobalState } from "../../global-state/context-provider";
import { API, TListProductionsResponse } from "../../api/api.ts";
import logger from "../../utils/logger.ts";

export type GetProductionListFilter = {
limit?: string;
offset?: string;
extended?: string;
};

const MAX_CONSECUTIVE_401_ERRORS = 10;

export const useFetchProductionList = (filter?: GetProductionListFilter) => {
const [productions, setProductions] = useState<TListProductionsResponse>();
const [doInitialLoad, setDoInitialLoad] = useState(true);
const [intervalLoad, setIntervalLoad] = useState<boolean>(false);
const [error, setError] = useState<Error | null>(null);
const consecutive401ErrorsRef = useRef(0);
const isPollingActiveRef = useRef(true);

const [{ reloadProductionList }, dispatch] = useGlobalState();

Expand All @@ -22,13 +27,21 @@ export const useFetchProductionList = (filter?: GetProductionListFilter) => {
// TODO improve performance: this makes the call 3 times
useEffect(() => {
let aborted = false;
if (

const shouldFetch =
reloadProductionList ||
intervalLoad ||
doInitialLoad ||
// offset-param is never present on launch-page
(filter?.offset ? manageProdPaginationUpdate : false)
) {
(filter?.offset ? manageProdPaginationUpdate : false);

if (shouldFetch) {
// Check if polling is active for interval loads
if (intervalLoad && !isPollingActiveRef.current) {
setIntervalLoad(false);
return;
}

const searchParams = new URLSearchParams(filter).toString();
API.listProductions({ searchParams })
.then((result) => {
Expand All @@ -43,8 +56,34 @@ export const useFetchProductionList = (filter?: GetProductionListFilter) => {
setIntervalLoad(false);
setDoInitialLoad(false);
setError(null);

// Reset error counter on successful fetch
consecutive401ErrorsRef.current = 0;
})
.catch((e) => {
if (aborted) return;

const errorMessage = e instanceof Error ? e.message : String(e);

// Check if this is a 401 unauthorized error
if (errorMessage.includes("Response Code: 401")) {
consecutive401ErrorsRef.current += 1;

logger.red(`Production list 401 error (${consecutive401ErrorsRef.current}/${MAX_CONSECUTIVE_401_ERRORS}): ${errorMessage}`);

// Stop polling after max consecutive 401 errors
if (consecutive401ErrorsRef.current >= MAX_CONSECUTIVE_401_ERRORS) {
isPollingActiveRef.current = false;
logger.red(`Production list polling stopped after ${MAX_CONSECUTIVE_401_ERRORS} consecutive 401 errors. Reauthentication required.`);
setIntervalLoad(false);
return;
}
} else {
// Reset 401 counter for non-401 errors
consecutive401ErrorsRef.current = 0;
}

// Handle errors as before
dispatch({
type: "API_NOT_AVAILABLE",
});
Expand All @@ -68,10 +107,27 @@ export const useFetchProductionList = (filter?: GetProductionListFilter) => {
manageProdPaginationUpdate,
]);

// Enhanced setIntervalLoad that respects polling state
const setIntervalLoadWithBackoff = (value: boolean) => {
if (value && !isPollingActiveRef.current) {
// Don't trigger interval load if polling is stopped due to 401 errors
return;
}
setIntervalLoad(value);
};

// Reset polling state when reloadProductionList or doInitialLoad changes
useEffect(() => {
if (reloadProductionList || doInitialLoad) {
consecutive401ErrorsRef.current = 0;
isPollingActiveRef.current = true;
}
}, [reloadProductionList, doInitialLoad]);

return {
productions,
doInitialLoad,
error,
setIntervalLoad,
setIntervalLoad: setIntervalLoadWithBackoff,
};
};
};
58 changes: 53 additions & 5 deletions src/components/production-line/use-heartbeat.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,68 @@
import { useEffect } from "react";
import { useEffect, useRef } from "react";
import { API } from "../../api/api.ts";
import { noop } from "../../helpers.ts";
import logger from "../../utils/logger.ts";

type TProps = { sessionId: string | null };

const MAX_CONSECUTIVE_401_ERRORS = 10;
const HEARTBEAT_INTERVAL = 10_000;

export const useHeartbeat = ({ sessionId }: TProps) => {
const consecutive401ErrorsRef = useRef(0);
const isPollingActiveRef = useRef(true);

useEffect(() => {
if (!sessionId) return noop;

const interval = window.setInterval(() => {
API.heartbeat({ sessionId }).catch(logger.red);
}, 10_000);
consecutive401ErrorsRef.current = 0;
isPollingActiveRef.current = true;

const performHeartbeat = async () => {
if (!isPollingActiveRef.current) {
return;
}

try {
await API.heartbeat({ sessionId });
// Reset error counter on successful heartbeat
consecutive401ErrorsRef.current = 0;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);

// Check if this is a 401 unauthorized error
if (errorMessage.includes("Response Code: 401")) {
consecutive401ErrorsRef.current += 1;

logger.red(`Heartbeat 401 error (${consecutive401ErrorsRef.current}/${MAX_CONSECUTIVE_401_ERRORS}): ${errorMessage}`);

// Stop polling after max consecutive 401 errors
if (consecutive401ErrorsRef.current >= MAX_CONSECUTIVE_401_ERRORS) {
isPollingActiveRef.current = false;
logger.red(`Heartbeat polling stopped after ${MAX_CONSECUTIVE_401_ERRORS} consecutive 401 errors. Reauthentication required.`);
return;
}
} else {
// Reset 401 counter for non-401 errors
consecutive401ErrorsRef.current = 0;
logger.red(errorMessage);
}
}
};

const interval = window.setInterval(performHeartbeat, HEARTBEAT_INTERVAL);

return () => {
window.clearInterval(interval);
};
}, [sessionId]);
};

// Provide a way to resume polling after successful reauthentication
const resumeHeartbeatPolling = () => {
consecutive401ErrorsRef.current = 0;
isPollingActiveRef.current = true;
logger.red("Heartbeat polling resumed after successful reauthentication");
};

return { resumeHeartbeatPolling };
};
53 changes: 45 additions & 8 deletions src/components/production-line/use-line-polling.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useEffect, useState, useRef } from "react";
import { noop } from "../../helpers.ts";
import { API } from "../../api/api.ts";
import { TJoinProductionOptions, TLine } from "./types.ts";
Expand All @@ -10,20 +10,54 @@ type TProps = {
joinProductionOptions: TJoinProductionOptions | null;
};

const MAX_CONSECUTIVE_401_ERRORS = 10;
const LINE_POLLING_INTERVAL = 1000;

export const useLinePolling = ({ callId, joinProductionOptions }: TProps) => {
const [line, setLine] = useState<TLine | null>(null);
const [, dispatch] = useGlobalState();
const consecutive401ErrorsRef = useRef(0);
const isPollingActiveRef = useRef(true);

useEffect(() => {
if (!joinProductionOptions) return noop;

const productionId = parseInt(joinProductionOptions.productionId, 10);
const lineId = parseInt(joinProductionOptions.lineId, 10);

const interval = window.setInterval(() => {
API.fetchProductionLine(productionId, lineId)
.then((l) => setLine(l))
.catch(() => {
consecutive401ErrorsRef.current = 0;
isPollingActiveRef.current = true;

const performLinePolling = async () => {
if (!isPollingActiveRef.current) {
return;
}

try {
const l = await API.fetchProductionLine(productionId, lineId);
setLine(l);
// Reset error counter on successful fetch
consecutive401ErrorsRef.current = 0;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);

// Check if this is a 401 unauthorized error
if (errorMessage.includes("Response Code: 401")) {
consecutive401ErrorsRef.current += 1;

logger.red(`Line polling 401 error (${consecutive401ErrorsRef.current}/${MAX_CONSECUTIVE_401_ERRORS}): ${errorMessage}`);

// Stop polling after max consecutive 401 errors
if (consecutive401ErrorsRef.current >= MAX_CONSECUTIVE_401_ERRORS) {
isPollingActiveRef.current = false;
logger.red(`Line polling stopped after ${MAX_CONSECUTIVE_401_ERRORS} consecutive 401 errors. Reauthentication required.`);
return;
}
} else {
// Reset 401 counter for non-401 errors
consecutive401ErrorsRef.current = 0;

// Handle non-401 errors as before
logger.red(
`Error fetching production line ${productionId}/${lineId}. For call-id: ${callId}`
);
Expand All @@ -36,13 +70,16 @@ export const useLinePolling = ({ callId, joinProductionOptions }: TProps) => {
),
},
});
});
}, 1000);
}
}
};

const interval = window.setInterval(performLinePolling, LINE_POLLING_INTERVAL);

return () => {
window.clearInterval(interval);
};
}, [callId, dispatch, joinProductionOptions]);

return line;
};
};
Loading