Poll when updating on server

This commit is contained in:
Zerebos
2026-05-21 02:43:06 -04:00
parent 745b6993de
commit b0efb183e8
3 changed files with 80 additions and 22 deletions
-14
View File
@@ -75,20 +75,6 @@ export const LIBRARY_UPDATE_STATUS = `
manga { id title thumbnailUrl unreadCount } manga { id title thumbnailUrl unreadCount }
} }
} }
}
`;
export const GET_LIBRARY_UPDATE_PANEL_STATUS = `
query GetLibraryUpdatePanelStatus {
libraryUpdateStatus {
jobsInfo {
isRunning
finishedJobs
totalJobs
skippedMangasCount
skippedCategoriesCount
}
}
lastUpdateTimestamp { lastUpdateTimestamp {
timestamp timestamp
} }
+1 -2
View File
@@ -10,8 +10,7 @@
| `GET_CATEGORIES` | — | All categories with order/settings and their assigned manga (minimal fields) | | `GET_CATEGORIES` | — | All categories with order/settings and their assigned manga (minimal fields) |
| `GET_DOWNLOADED_CHAPTERS_PAGES` | — | Page counts for all downloaded chapters — used for storage stats | | `GET_DOWNLOADED_CHAPTERS_PAGES` | — | Page counts for all downloaded chapters — used for storage stats |
| `GET_DOWNLOADS_PATH` | — | `downloadsPath` and `localSourcePath` from settings | | `GET_DOWNLOADS_PATH` | — | `downloadsPath` and `localSourcePath` from settings |
| `LIBRARY_UPDATE_STATUS` | — | Current library update job `jobsInfo` progress and `mangaUpdates` list with new chapters | | `LIBRARY_UPDATE_STATUS` | — | Current library update job (`jobsInfo`, `mangaUpdates`) plus `lastUpdateTimestamp` for server-side update timing |
| `GET_LIBRARY_UPDATE_PANEL_STATUS` | — | Library updater status + server `lastUpdateTimestamp` for UI status displays |
| `GET_RESTORE_STATUS` | `id: String!` | Backup restore job status by job ID — `mangaProgress`, `state`, `totalManga` | | `GET_RESTORE_STATUS` | `id: String!` | Backup restore job status by job ID — `mangaProgress`, `state`, `totalManga` |
| `VALIDATE_BACKUP` | `backup: Upload!` | Validate a backup file before restore — returns missing sources and trackers | | `VALIDATE_BACKUP` | `backup: Upload!` | Validate a backup file before restore — returns missing sources and trackers |
| `MANGAS_BY_GENRE` | `filter: MangaFilterInput`, `first: Int`, `offset: Int` | Paginated manga filtered by genre, ordered by `IN_LIBRARY_AT DESC` | | `MANGAS_BY_GENRE` | `filter: MangaFilterInput`, `first: Int`, `offset: Int` | Paginated manga filtered by genre, ordered by `IN_LIBRARY_AT DESC` |
@@ -2,7 +2,7 @@
import { onMount, onDestroy } from "svelte"; import { onMount, onDestroy } from "svelte";
import { BookOpen, CircleNotch } from "phosphor-svelte"; import { BookOpen, CircleNotch } from "phosphor-svelte";
import { gql } from "@api/client"; import { gql } from "@api/client";
import { GET_RECENTLY_UPDATED, GET_CHAPTERS, GET_LIBRARY_UPDATE_PANEL_STATUS } from "@api/queries"; import { GET_RECENTLY_UPDATED, GET_CHAPTERS, LIBRARY_UPDATE_STATUS } from "@api/queries";
import { cache, CACHE_GROUPS, CACHE_KEYS } from "@core/cache"; import { cache, CACHE_GROUPS, CACHE_KEYS } from "@core/cache";
import { store, openReader, setActiveManga, addToast } from "@store/state.svelte"; import { store, openReader, setActiveManga, addToast } from "@store/state.svelte";
import { dayLabel } from "@core/util"; import { dayLabel } from "@core/util";
@@ -31,9 +31,13 @@
let openingId = $state<number | null>(null); let openingId = $state<number | null>(null);
let updaterRunning = $state(false); let updaterRunning = $state(false);
let lastUpdatedTs = $state<number | null>(null); let lastUpdatedTs = $state<number | null>(null);
let updaterFinishedJobs = $state<number | null>(null);
let updaterTotalJobs = $state<number | null>(null);
let ctrl: AbortController | null = null; let ctrl: AbortController | null = null;
let statusPollTimer: ReturnType<typeof setTimeout> | null = null;
const RECENT_UPDATES_TTL_MS = 60 * 1_000; const RECENT_UPDATES_TTL_MS = 60 * 1_000;
const UPDATE_STATUS_POLL_MS = 2_000;
onMount(() => { onMount(() => {
onRegisterRefresh?.(() => loadUpdates(true)); onRegisterRefresh?.(() => loadUpdates(true));
@@ -42,6 +46,7 @@
onDestroy(() => { onDestroy(() => {
ctrl?.abort(); ctrl?.abort();
stopStatusPolling();
}); });
function fetchedAtMs(item: Pick<RecentUpdate, "fetchedAt">): number { function fetchedAtMs(item: Pick<RecentUpdate, "fetchedAt">): number {
@@ -71,6 +76,12 @@
: null : null
); );
const updaterProgressLabel = $derived(
typeof updaterFinishedJobs === "number" && typeof updaterTotalJobs === "number" && updaterTotalJobs > 0
? `${updaterFinishedJobs}/${updaterTotalJobs}`
: null
);
function parseServerTimestamp(value: unknown): number | null { function parseServerTimestamp(value: unknown): number | null {
if (typeof value === "number") return Number.isFinite(value) ? value : null; if (typeof value === "number") return Number.isFinite(value) ? value : null;
if (typeof value === "string") { if (typeof value === "string") {
@@ -82,6 +93,64 @@
return null; return null;
} }
function applyUpdateStatus(statusRes: {
libraryUpdateStatus: {
jobsInfo: {
isRunning: boolean;
finishedJobs?: number;
totalJobs?: number;
};
};
lastUpdateTimestamp: { timestamp: string | number | null } | null;
} | null) {
const jobsInfo = statusRes?.libraryUpdateStatus.jobsInfo;
updaterRunning = jobsInfo?.isRunning ?? false;
updaterFinishedJobs = typeof jobsInfo?.finishedJobs === "number" ? jobsInfo.finishedJobs : null;
updaterTotalJobs = typeof jobsInfo?.totalJobs === "number" ? jobsInfo.totalJobs : null;
lastUpdatedTs = parseServerTimestamp(statusRes?.lastUpdateTimestamp?.timestamp ?? null);
}
function stopStatusPolling() {
if (!statusPollTimer) return;
clearTimeout(statusPollTimer);
statusPollTimer = null;
}
function scheduleStatusPoll() {
if (statusPollTimer) return;
const tick = async () => {
statusPollTimer = null;
try {
const statusRes = await gql<{
libraryUpdateStatus: {
jobsInfo: {
isRunning: boolean;
finishedJobs: number;
totalJobs: number;
};
};
lastUpdateTimestamp: { timestamp: string | number | null } | null;
}>(LIBRARY_UPDATE_STATUS, {});
const wasRunning = updaterRunning;
applyUpdateStatus(statusRes);
if (updaterRunning) {
statusPollTimer = setTimeout(tick, UPDATE_STATUS_POLL_MS);
} else if (wasRunning) {
void loadUpdates(true);
}
} catch {
if (updaterRunning) {
statusPollTimer = setTimeout(tick, UPDATE_STATUS_POLL_MS);
}
}
};
statusPollTimer = setTimeout(tick, UPDATE_STATUS_POLL_MS);
}
function mangaStub(item: RecentUpdate): Manga { function mangaStub(item: RecentUpdate): Manga {
return { return {
id: item.manga?.id ?? item.mangaId, id: item.manga?.id ?? item.mangaId,
@@ -120,11 +189,12 @@
jobsInfo: { isRunning: boolean }; jobsInfo: { isRunning: boolean };
}; };
lastUpdateTimestamp: { timestamp: string | number | null } | null; lastUpdateTimestamp: { timestamp: string | number | null } | null;
}>(GET_LIBRARY_UPDATE_PANEL_STATUS, {}, nextCtrl.signal).catch(() => null), }>(LIBRARY_UPDATE_STATUS, {}, nextCtrl.signal).catch(() => null),
]); ]);
updaterRunning = statusRes?.libraryUpdateStatus.jobsInfo.isRunning ?? false; applyUpdateStatus(statusRes);
lastUpdatedTs = parseServerTimestamp(statusRes?.lastUpdateTimestamp?.timestamp ?? null); if (updaterRunning) scheduleStatusPoll();
else stopStatusPolling();
if (nextCtrl.signal.aborted) return; if (nextCtrl.signal.aborted) return;
@@ -137,6 +207,9 @@
updates = []; updates = [];
updaterRunning = false; updaterRunning = false;
lastUpdatedTs = null; lastUpdatedTs = null;
updaterFinishedJobs = null;
updaterTotalJobs = null;
stopStatusPolling();
} finally { } finally {
if (!nextCtrl.signal.aborted) loading = false; if (!nextCtrl.signal.aborted) loading = false;
} }
@@ -171,9 +244,9 @@
<div class="root anim-fade-in"> <div class="root anim-fade-in">
<div class="bar-wrap"> <div class="bar-wrap">
<div class="status-bar"> <div class="status-bar">
<div class="status-dot" class:active={loading}></div> <div class="status-dot" class:active={loading || updaterRunning}></div>
<span class="status-text"> <span class="status-text">
{#if loading}Checking for updates…{:else if error}Update check failed{:else if updaterRunning}Library update in progress{:else}Up to date{/if} {#if loading}Checking for updates…{:else if error}Update check failed{:else if updaterRunning}Library update in progress...{#if updaterProgressLabel} ({updaterProgressLabel}){/if}{:else}Up to date{/if}
</span> </span>
<div class="status-right"> <div class="status-right">
{#if !loading && lastUpdatedLabel} {#if !loading && lastUpdatedLabel}