Improve ui polling code to prevent poll buildup
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, use } from 'react';
|
import { useState, use } from 'react';
|
||||||
import { FaChevronLeft } from 'react-icons/fa';
|
import { FaChevronLeft } from 'react-icons/fa';
|
||||||
import { MdDashboard, MdImage, MdShowChart, MdCode, MdExtension } from 'react-icons/md';
|
import { MdDashboard, MdImage, MdShowChart, MdCode, MdExtension } from 'react-icons/md';
|
||||||
import { Button } from '@headlessui/react';
|
import { Button } from '@headlessui/react';
|
||||||
import { TopBar, MainContent } from '@/components/layout';
|
import { TopBar, MainContent } from '@/components/layout';
|
||||||
import useJob from '@/hooks/useJob';
|
import useJob from '@/hooks/useJob';
|
||||||
|
import usePollLoop from '@/hooks/usePollLoop';
|
||||||
import SampleImages, { SampleImagesMenu } from '@/components/SampleImages';
|
import SampleImages, { SampleImagesMenu } from '@/components/SampleImages';
|
||||||
import JobOverview from '@/components/JobOverview';
|
import JobOverview from '@/components/JobOverview';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
@@ -77,18 +78,16 @@ export default function JobPage({ params }: { params: { jobID: string } }) {
|
|||||||
const [hasPlugin, setHasPlugin] = useState(false);
|
const [hasPlugin, setHasPlugin] = useState(false);
|
||||||
|
|
||||||
// poll for plugin.html in the job folder; show the Plugin tab if it exists
|
// poll for plugin.html in the job folder; show the Plugin tab if it exists
|
||||||
useEffect(() => {
|
usePollLoop(
|
||||||
const checkPlugin = () => {
|
() =>
|
||||||
apiClient
|
apiClient
|
||||||
.get(`/api/jobs/${jobID}/plugin?check=1`)
|
.get(`/api/jobs/${jobID}/plugin?check=1`)
|
||||||
.then(res => res.data)
|
.then(res => res.data)
|
||||||
.then(data => setHasPlugin(!!data.exists))
|
.then(data => setHasPlugin(!!data.exists))
|
||||||
.catch(() => {});
|
.catch(() => {}),
|
||||||
};
|
5000,
|
||||||
checkPlugin();
|
[jobID],
|
||||||
const interval = setInterval(checkPlugin, 5000);
|
);
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [jobID]);
|
|
||||||
|
|
||||||
const page = pages.find(p => p.value === pageKey);
|
const page = pages.find(p => p.value === pageKey);
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
import React, { useState, useRef, useMemo } from 'react';
|
||||||
import { GPUApiResponse } from '@/types';
|
import { GPUApiResponse } from '@/types';
|
||||||
import Loading from '@/components/Loading';
|
import Loading from '@/components/Loading';
|
||||||
import GPUWidget from '@/components/GPUWidget';
|
import GPUWidget from '@/components/GPUWidget';
|
||||||
import { apiClient } from '@/utils/api';
|
import { apiClient } from '@/utils/api';
|
||||||
|
import usePollLoop from '@/hooks/usePollLoop';
|
||||||
|
|
||||||
const GpuMonitor: React.FC = () => {
|
const GpuMonitor: React.FC = () => {
|
||||||
const [gpuData, setGpuData] = useState<GPUApiResponse | null>(null);
|
const [gpuData, setGpuData] = useState<GPUApiResponse | null>(null);
|
||||||
@@ -11,39 +12,32 @@ const GpuMonitor: React.FC = () => {
|
|||||||
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
|
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
|
||||||
const isFetchingGpuRef = useRef(false);
|
const isFetchingGpuRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchGpuInfo = () => {
|
||||||
const fetchGpuInfo = async () => {
|
if (isFetchingGpuRef.current) {
|
||||||
if (isFetchingGpuRef.current) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
setLoading(true);
|
||||||
setLoading(true);
|
isFetchingGpuRef.current = true;
|
||||||
isFetchingGpuRef.current = true;
|
return apiClient
|
||||||
apiClient
|
.get('/api/gpu')
|
||||||
.get('/api/gpu')
|
.then(res => res.data)
|
||||||
.then(res => res.data)
|
.then(data => {
|
||||||
.then(data => {
|
setGpuData(data);
|
||||||
setGpuData(data);
|
setLastUpdated(new Date());
|
||||||
setLastUpdated(new Date());
|
setError(null);
|
||||||
setError(null);
|
})
|
||||||
})
|
.catch(err => {
|
||||||
.catch(err => {
|
setError(`Failed to fetch GPU data: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
setError(`Failed to fetch GPU data: ${err instanceof Error ? err.message : String(err)}`);
|
})
|
||||||
})
|
.finally(() => {
|
||||||
.finally(() => {
|
isFetchingGpuRef.current = false;
|
||||||
isFetchingGpuRef.current = false;
|
setLoading(false);
|
||||||
setLoading(false);
|
});
|
||||||
});
|
};
|
||||||
};
|
|
||||||
|
|
||||||
// Fetch immediately on component mount
|
// Fetch every second, but only schedule the next fetch after the current
|
||||||
fetchGpuInfo();
|
// one finishes so slow responses can't stack requests
|
||||||
|
usePollLoop(fetchGpuInfo, 1000);
|
||||||
// Set up interval to fetch every 1 seconds
|
|
||||||
const intervalId = setInterval(fetchGpuInfo, 1000);
|
|
||||||
|
|
||||||
// Clean up interval on component unmount
|
|
||||||
return () => clearInterval(intervalId);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getGridClasses = (gpuCount: number): string => {
|
const getGridClasses = (gpuCount: number): string => {
|
||||||
switch (gpuCount) {
|
switch (gpuCount) {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Wallet } from 'lucide-react';
|
import { Wallet } from 'lucide-react';
|
||||||
import { apiClient } from '@/utils/api';
|
import { apiClient } from '@/utils/api';
|
||||||
|
import usePollLoop from '@/hooks/usePollLoop';
|
||||||
|
|
||||||
interface BalanceResponse {
|
interface BalanceResponse {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -17,26 +18,14 @@ interface BalanceResponse {
|
|||||||
export default function OstrisCloudBalance() {
|
export default function OstrisCloudBalance() {
|
||||||
const [data, setData] = useState<BalanceResponse | null>(null);
|
const [data, setData] = useState<BalanceResponse | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
usePollLoop(async () => {
|
||||||
let cancelled = false;
|
try {
|
||||||
|
const res = await apiClient.get<BalanceResponse>('/api/ostris_cloud');
|
||||||
const fetchBalance = async () => {
|
setData(res.data);
|
||||||
try {
|
} catch {
|
||||||
const res = await apiClient.get<BalanceResponse>('/api/ostris_cloud');
|
// ignore — keep last known value
|
||||||
if (!cancelled) setData(res.data);
|
}
|
||||||
} catch {
|
}, 60_000);
|
||||||
// ignore — keep last known value
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchBalance();
|
|
||||||
const interval = setInterval(fetchBalance, 60_000);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
clearInterval(interval);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!data || !data.enabled || !data.appUrl) return null;
|
if (!data || !data.enabled || !data.appUrl) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { CpuInfo } from '@/types';
|
import { CpuInfo } from '@/types';
|
||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { apiClient } from '@/utils/api';
|
import { apiClient } from '@/utils/api';
|
||||||
|
import usePollLoop from '@/hooks/usePollLoop';
|
||||||
|
|
||||||
export default function useCPUInfo(reloadInterval: null | number = null) {
|
export default function useCPUInfo(reloadInterval: null | number = null) {
|
||||||
const [cpuInfo, setCpuInfo] = useState<CpuInfo | null>(null);
|
const [cpuInfo, setCpuInfo] = useState<CpuInfo | null>(null);
|
||||||
@@ -23,22 +24,7 @@ export default function useCPUInfo(reloadInterval: null | number = null) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
usePollLoop(fetchCpuInfo, reloadInterval);
|
||||||
// Fetch immediately on component mount
|
|
||||||
fetchCpuInfo();
|
|
||||||
|
|
||||||
// Set up interval if specified
|
|
||||||
if (reloadInterval) {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
fetchCpuInfo();
|
|
||||||
}, reloadInterval);
|
|
||||||
|
|
||||||
// Cleanup interval on unmount
|
|
||||||
return () => {
|
|
||||||
clearInterval(interval);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [reloadInterval]); // Added dependencies
|
|
||||||
|
|
||||||
return { cpuInfo, isCPUInfoLoaded, status, refreshCpuInfo: fetchCpuInfo };
|
return { cpuInfo, isCPUInfoLoaded, status, refreshCpuInfo: fetchCpuInfo };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState, useRef } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
import { apiClient } from '@/utils/api';
|
import { apiClient } from '@/utils/api';
|
||||||
|
import usePollLoop from '@/hooks/usePollLoop';
|
||||||
|
|
||||||
interface FileObject {
|
interface FileObject {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -19,7 +20,7 @@ export default function useFilesList(jobID: string, reloadInterval: null | numbe
|
|||||||
loadStatus = 'refreshing';
|
loadStatus = 'refreshing';
|
||||||
}
|
}
|
||||||
setStatus(loadStatus);
|
setStatus(loadStatus);
|
||||||
apiClient
|
return apiClient
|
||||||
.get(`/api/jobs/${jobID}/files`)
|
.get(`/api/jobs/${jobID}/files`)
|
||||||
.then(res => res.data)
|
.then(res => res.data)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -36,19 +37,7 @@ export default function useFilesList(jobID: string, reloadInterval: null | numbe
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
usePollLoop(refreshFiles, reloadInterval, [jobID]);
|
||||||
refreshFiles();
|
|
||||||
|
|
||||||
if (reloadInterval) {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
refreshFiles();
|
|
||||||
}, reloadInterval);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(interval);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [jobID]);
|
|
||||||
|
|
||||||
return { files, setFiles, status, refreshFiles };
|
return { files, setFiles, status, refreshFiles };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { GPUApiResponse, GpuInfo } from '@/types';
|
import { GPUApiResponse, GpuInfo } from '@/types';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { apiClient } from '@/utils/api';
|
import { apiClient } from '@/utils/api';
|
||||||
|
import usePollLoop from '@/hooks/usePollLoop';
|
||||||
|
|
||||||
export default function useGPUInfo(gpuIds: null | number[] = null, reloadInterval: null | number = null) {
|
export default function useGPUInfo(gpuIds: null | number[] = null, reloadInterval: null | number = null) {
|
||||||
const [gpuList, setGpuList] = useState<GpuInfo[]>([]);
|
const [gpuList, setGpuList] = useState<GpuInfo[]>([]);
|
||||||
@@ -27,22 +28,7 @@ export default function useGPUInfo(gpuIds: null | number[] = null, reloadInterva
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
usePollLoop(fetchGpuInfo, reloadInterval, [gpuIds]);
|
||||||
// Fetch immediately on component mount
|
|
||||||
fetchGpuInfo();
|
|
||||||
|
|
||||||
// Set up interval if specified
|
|
||||||
if (reloadInterval) {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
fetchGpuInfo();
|
|
||||||
}, reloadInterval);
|
|
||||||
|
|
||||||
// Cleanup interval on unmount
|
|
||||||
return () => {
|
|
||||||
clearInterval(interval);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [gpuIds, reloadInterval]); // Added dependencies
|
|
||||||
|
|
||||||
// If the initial fetch failed (e.g. nvidia-smi was transiently busy on startup),
|
// If the initial fetch failed (e.g. nvidia-smi was transiently busy on startup),
|
||||||
// retry automatically so users don't have to reload the page to see their GPUs.
|
// retry automatically so users don't have to reload the page to see their GPUs.
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Job } from '@prisma/client';
|
import { Job } from '@prisma/client';
|
||||||
import { apiClient } from '@/utils/api';
|
import { apiClient } from '@/utils/api';
|
||||||
|
import usePollLoop from '@/hooks/usePollLoop';
|
||||||
|
|
||||||
export default function useJob(jobID: string, reloadInterval: null | number = null) {
|
export default function useJob(jobID: string, reloadInterval: null | number = null) {
|
||||||
const [job, setJob] = useState<Job | null>(null);
|
const [job, setJob] = useState<Job | null>(null);
|
||||||
@@ -10,7 +11,7 @@ export default function useJob(jobID: string, reloadInterval: null | number = nu
|
|||||||
|
|
||||||
const refreshJob = () => {
|
const refreshJob = () => {
|
||||||
setStatus('loading');
|
setStatus('loading');
|
||||||
apiClient
|
return apiClient
|
||||||
.get(`/api/jobs?id=${jobID}`)
|
.get(`/api/jobs?id=${jobID}`)
|
||||||
.then(res => res.data)
|
.then(res => res.data)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -24,19 +25,7 @@ export default function useJob(jobID: string, reloadInterval: null | number = nu
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
usePollLoop(refreshJob, reloadInterval, [jobID]);
|
||||||
refreshJob();
|
|
||||||
|
|
||||||
if (reloadInterval) {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
refreshJob();
|
|
||||||
}, reloadInterval);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(interval);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [jobID]);
|
|
||||||
|
|
||||||
return { job, setJob, status, refreshJob };
|
return { job, setJob, status, refreshJob };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Job } from '@prisma/client';
|
import { Job } from '@prisma/client';
|
||||||
import { apiClient } from '@/utils/api';
|
import { apiClient } from '@/utils/api';
|
||||||
|
import usePollLoop from '@/hooks/usePollLoop';
|
||||||
|
|
||||||
export default function useJobByRef(jobRef: string | null, reloadInterval: null | number = null) {
|
export default function useJobByRef(jobRef: string | null, reloadInterval: null | number = null) {
|
||||||
const [job, setJob] = useState<Job | null>(null);
|
const [job, setJob] = useState<Job | null>(null);
|
||||||
@@ -10,7 +11,7 @@ export default function useJobByRef(jobRef: string | null, reloadInterval: null
|
|||||||
|
|
||||||
const refreshJob = () => {
|
const refreshJob = () => {
|
||||||
setStatus('loading');
|
setStatus('loading');
|
||||||
apiClient
|
return apiClient
|
||||||
.get(`/api/jobs?job_ref=${jobRef}`)
|
.get(`/api/jobs?job_ref=${jobRef}`)
|
||||||
.then(res => res.data)
|
.then(res => res.data)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -24,19 +25,7 @@ export default function useJobByRef(jobRef: string | null, reloadInterval: null
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
usePollLoop(refreshJob, reloadInterval, [jobRef]);
|
||||||
refreshJob();
|
|
||||||
|
|
||||||
if (reloadInterval) {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
refreshJob();
|
|
||||||
}, reloadInterval);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(interval);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [jobRef, reloadInterval]);
|
|
||||||
|
|
||||||
return { job, setJob, status, refreshJob };
|
return { job, setJob, status, refreshJob };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useEffect, useState, useRef } from 'react';
|
import { useEffect, useState, useRef } from 'react';
|
||||||
import { apiClient } from '@/utils/api';
|
import { apiClient } from '@/utils/api';
|
||||||
import { TerminalEmulator } from '@/utils/terminalEmulator';
|
import { TerminalEmulator } from '@/utils/terminalEmulator';
|
||||||
|
import usePollLoop from '@/hooks/usePollLoop';
|
||||||
|
|
||||||
interface FileObject {
|
interface FileObject {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -38,7 +39,7 @@ export default function useJobLog(jobID: string, reloadInterval: null | number =
|
|||||||
}
|
}
|
||||||
setStatus(loadStatus);
|
setStatus(loadStatus);
|
||||||
const offset = offsetRef.current;
|
const offset = offsetRef.current;
|
||||||
apiClient
|
return apiClient
|
||||||
.get(`/api/jobs/${jobID}/log`, offset !== null ? { params: { offset } } : undefined)
|
.get(`/api/jobs/${jobID}/log`, offset !== null ? { params: { offset } } : undefined)
|
||||||
.then(res => res.data)
|
.then(res => res.data)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -62,25 +63,17 @@ export default function useJobLog(jobID: string, reloadInterval: null | number =
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// New job — start fresh. Declared before the poll loop so the reset runs
|
||||||
|
// before the first fetch when jobID changes.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// New job — start fresh.
|
|
||||||
offsetRef.current = null;
|
offsetRef.current = null;
|
||||||
didInitialLoadRef.current = false;
|
didInitialLoadRef.current = false;
|
||||||
inFlightRef.current = false;
|
inFlightRef.current = false;
|
||||||
terminalRef.current?.reset();
|
terminalRef.current?.reset();
|
||||||
setLog('');
|
setLog('');
|
||||||
refresh();
|
|
||||||
|
|
||||||
if (reloadInterval) {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
refresh();
|
|
||||||
}, reloadInterval);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(interval);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [jobID]);
|
}, [jobID]);
|
||||||
|
|
||||||
|
usePollLoop(refresh, reloadInterval, [jobID]);
|
||||||
|
|
||||||
return { log, status, refresh };
|
return { log, status, refresh };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
|
import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
|
||||||
import { apiClient } from '@/utils/api';
|
import { apiClient } from '@/utils/api';
|
||||||
|
import usePollLoop from '@/hooks/usePollLoop';
|
||||||
|
|
||||||
export interface LossPoint {
|
export interface LossPoint {
|
||||||
step: number;
|
step: number;
|
||||||
@@ -115,24 +116,17 @@ export default function useJobLossLog(jobID: string, reloadInterval: null | numb
|
|||||||
}
|
}
|
||||||
}, [jobID, reloadInterval]);
|
}, [jobID, reloadInterval]);
|
||||||
|
|
||||||
|
// reset when job changes. Declared before the poll loop so the reset runs
|
||||||
|
// before the first fetch when jobID changes.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// reset when job changes
|
|
||||||
didInitialLoadRef.current = false;
|
didInitialLoadRef.current = false;
|
||||||
lastStepByKeyRef.current = {};
|
lastStepByKeyRef.current = {};
|
||||||
setSeries({});
|
setSeries({});
|
||||||
setKeys([]);
|
setKeys([]);
|
||||||
setStatus('idle');
|
setStatus('idle');
|
||||||
|
}, [jobID]);
|
||||||
|
|
||||||
refreshLoss();
|
usePollLoop(refreshLoss, reloadInterval, [jobID]);
|
||||||
|
|
||||||
if (reloadInterval) {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
refreshLoss();
|
|
||||||
}, reloadInterval);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}
|
|
||||||
}, [jobID, reloadInterval, refreshLoss]);
|
|
||||||
|
|
||||||
return { series, keys, lossKeys, status, refreshLoss, setSeries };
|
return { series, keys, lossKeys, status, refreshLoss, setSeries };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { Job } from '@prisma/client';
|
import { Job } from '@prisma/client';
|
||||||
import { apiClient } from '@/utils/api';
|
import { apiClient } from '@/utils/api';
|
||||||
|
import usePollLoop from '@/hooks/usePollLoop';
|
||||||
|
|
||||||
type UseJobsListProps = {
|
type UseJobsListProps = {
|
||||||
onlyActive?: boolean;
|
onlyActive?: boolean;
|
||||||
@@ -30,7 +31,7 @@ export default function useJobsList({
|
|||||||
if (onlyActive) {
|
if (onlyActive) {
|
||||||
params.only_active = 'true';
|
params.only_active = 'true';
|
||||||
}
|
}
|
||||||
apiClient
|
return apiClient
|
||||||
.get('/api/jobs', { params })
|
.get('/api/jobs', { params })
|
||||||
.then(res => res.data)
|
.then(res => res.data)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -51,16 +52,7 @@ export default function useJobsList({
|
|||||||
isFetchingRef.current = false;
|
isFetchingRef.current = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
usePollLoop(refreshJobs, reloadInterval);
|
||||||
refreshJobs();
|
|
||||||
|
|
||||||
if (reloadInterval) {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
refreshJobs();
|
|
||||||
}, reloadInterval);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { jobs, setJobs, status, refreshJobs };
|
return { jobs, setJobs, status, refreshJobs };
|
||||||
}
|
}
|
||||||
|
|||||||
48
ui/src/hooks/usePollLoop.tsx
Normal file
48
ui/src/hooks/usePollLoop.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Polling loop that schedules the next run only after the current one
|
||||||
|
* settles, so a slow server can't stack overlapping requests the way
|
||||||
|
* setInterval does. Worst case the UI just updates late.
|
||||||
|
*
|
||||||
|
* - `fn` should return its promise so we know when the request finished.
|
||||||
|
* - Errors from `fn` are caught here so a failed poll never kills the loop.
|
||||||
|
* - `intervalMs` of null/0 runs `fn` once with no polling (matches the old
|
||||||
|
* reloadInterval=null behavior).
|
||||||
|
* - Changing `deps` (or `intervalMs`) restarts the loop, running `fn`
|
||||||
|
* immediately.
|
||||||
|
*/
|
||||||
|
export default function usePollLoop(
|
||||||
|
fn: () => void | Promise<unknown>,
|
||||||
|
intervalMs: number | null | undefined,
|
||||||
|
deps: unknown[] = [],
|
||||||
|
) {
|
||||||
|
// always call the latest fn without restarting the loop on every render
|
||||||
|
const fnRef = useRef(fn);
|
||||||
|
fnRef.current = fn;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let stopped = false;
|
||||||
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
|
const tick = async () => {
|
||||||
|
try {
|
||||||
|
await fnRef.current();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Poll error:', error);
|
||||||
|
}
|
||||||
|
if (stopped || !intervalMs) return;
|
||||||
|
timeout = setTimeout(tick, intervalMs);
|
||||||
|
};
|
||||||
|
|
||||||
|
tick();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
stopped = true;
|
||||||
|
if (timeout) clearTimeout(timeout);
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [intervalMs, ...deps]);
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { apiClient } from '@/utils/api';
|
import { apiClient } from '@/utils/api';
|
||||||
|
import usePollLoop from '@/hooks/usePollLoop';
|
||||||
|
|
||||||
export default function useSampleImages(jobID: string, reloadInterval: null | number = null) {
|
export default function useSampleImages(jobID: string, reloadInterval: null | number = null) {
|
||||||
const [sampleImages, setSampleImages] = useState<string[]>([]);
|
const [sampleImages, setSampleImages] = useState<string[]>([]);
|
||||||
@@ -9,7 +10,7 @@ export default function useSampleImages(jobID: string, reloadInterval: null | nu
|
|||||||
|
|
||||||
const refreshSampleImages = () => {
|
const refreshSampleImages = () => {
|
||||||
setStatus('loading');
|
setStatus('loading');
|
||||||
apiClient
|
return apiClient
|
||||||
.get(`/api/jobs/${jobID}/samples`)
|
.get(`/api/jobs/${jobID}/samples`)
|
||||||
.then(res => res.data)
|
.then(res => res.data)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -25,19 +26,7 @@ export default function useSampleImages(jobID: string, reloadInterval: null | nu
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
usePollLoop(refreshSampleImages, reloadInterval, [jobID]);
|
||||||
refreshSampleImages();
|
|
||||||
|
|
||||||
if (reloadInterval) {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
refreshSampleImages();
|
|
||||||
}, reloadInterval);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(interval);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [jobID]);
|
|
||||||
|
|
||||||
return { sampleImages, setSampleImages, status, refreshSampleImages };
|
return { sampleImages, setSampleImages, status, refreshSampleImages };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user