diff --git a/apps/web/src/app/server-storage/server-instance-probe.spec.ts b/apps/web/src/app/server-storage/server-instance-probe.spec.ts index 682857f9a..433346ab8 100644 --- a/apps/web/src/app/server-storage/server-instance-probe.spec.ts +++ b/apps/web/src/app/server-storage/server-instance-probe.spec.ts @@ -1,3 +1,4 @@ +import { markSlowStart } from '@iptvnator/services'; import { probeServerInstance } from './server-instance-probe'; const instance = { @@ -55,6 +56,28 @@ describe('probeServerInstance', () => { expect(fetchFn.mock.calls[0][0]).toBe('/api/v1/instance'); }); + it('waits longer once after a reload for a server that answered late', async () => { + const timeout = jest.spyOn(AbortSignal, 'timeout'); + try { + markSlowStart(); + await probeServerInstance({ + ...config, + fetchFn: answer(instance) as never, + }); + await probeServerInstance({ + ...config, + fetchFn: answer(instance) as never, + }); + + // A slow server otherwise times out again and reloads forever. + expect(timeout.mock.calls.map(([ms]) => ms)).toEqual([ + 20_000, 2_500, + ]); + } finally { + timeout.mockRestore(); + } + }); + it('reports no server storage for an answer without it', async () => { for (const fetchFn of [ answer({ ...instance, storage: 'none' }), diff --git a/apps/web/src/app/server-storage/server-instance-probe.ts b/apps/web/src/app/server-storage/server-instance-probe.ts index f95ba8f1c..858e696d2 100644 --- a/apps/web/src/app/server-storage/server-instance-probe.ts +++ b/apps/web/src/app/server-storage/server-instance-probe.ts @@ -1,8 +1,10 @@ -import { RuntimeCapabilitiesService } from '@iptvnator/services'; +import { RuntimeCapabilitiesService, takeSlowStart } from '@iptvnator/services'; import type { ServerInstanceInfo } from '@iptvnator/shared/interfaces'; import type { IptvnatorRuntimeConfig } from '../services/runtime-config'; const PROBE_TIMEOUT_MS = 2_500; +/** After a reload for a server that answered later: as long as requests. */ +const SLOW_PROBE_TIMEOUT_MS = 20_000; /** * - `skipped`: no runtime backend URL, or Electron. @@ -47,7 +49,11 @@ export async function probeServerInstance( cache: 'no-store', signal: typeof AbortSignal.timeout === 'function' - ? AbortSignal.timeout(PROBE_TIMEOUT_MS) + ? AbortSignal.timeout( + takeSlowStart() + ? SLOW_PROBE_TIMEOUT_MS + : PROBE_TIMEOUT_MS + ) : undefined, }); } catch { diff --git a/docs/architecture/pwa-self-hosted.md b/docs/architecture/pwa-self-hosted.md index fab717a52..54d1cb2f0 100644 --- a/docs/architecture/pwa-self-hosted.md +++ b/docs/architecture/pwa-self-hosted.md @@ -347,7 +347,9 @@ settings and guards read them in every build. database (backend URL or instance id) than the one it remembers; sign-in therefore remembers the server along with the token, or reports storage trouble. A page started from memory first checks that the instance at the - address is the remembered one (`ensureVerified`), and reloads if not, + address is the remembered one (`ensureVerified`), and reloads if not + (a reload after a server answered late gives the next start probe as + long as requests take, once, so a slow server does not reload forever), before sign-in sends the token. So does a page that went offline (any backend call can tell: the storage API with its page-hide `keepalive` writes, `HttpClient` calls through the interceptor and `PwaService`'s raw diff --git a/libs/services/src/index.ts b/libs/services/src/index.ts index 25fca28bd..9cd5bb1a0 100644 --- a/libs/services/src/index.ts +++ b/libs/services/src/index.ts @@ -35,3 +35,4 @@ export * from './lib/translation-tick'; export * from './lib/user-data-storage'; export * from './lib/server-storage-status.service'; export * from './lib/server-access-token'; +export * from './lib/server-slow-start'; diff --git a/libs/services/src/lib/server-slow-start.ts b/libs/services/src/lib/server-slow-start.ts new file mode 100644 index 000000000..8d3b25ef2 --- /dev/null +++ b/libs/services/src/lib/server-slow-start.ts @@ -0,0 +1,26 @@ +/** + * A page started from memory reloads once the server answers + * (`ServerStorageSession`), but a server slower than the start probe would + * time it out again and reload forever: the reloaded page's probe waits as + * long as requests do, once. + */ +const SLOW_START_KEY = 'iptvnator-server:slow-start'; + +export function markSlowStart(): void { + try { + sessionStorage.setItem(SLOW_START_KEY, '1'); + } catch { + // Without session storage the next probe keeps its short wait. + } +} + +/** Whether this page load follows such a reload; asks only once. */ +export function takeSlowStart(): boolean { + try { + const slow = sessionStorage.getItem(SLOW_START_KEY) !== null; + sessionStorage.removeItem(SLOW_START_KEY); + return slow; + } catch { + return false; + } +} diff --git a/libs/services/src/lib/server-storage/server-storage-session.spec.ts b/libs/services/src/lib/server-storage/server-storage-session.spec.ts index 206dc490d..333bb355a 100644 --- a/libs/services/src/lib/server-storage/server-storage-session.spec.ts +++ b/libs/services/src/lib/server-storage/server-storage-session.spec.ts @@ -5,6 +5,7 @@ import { TranslateService } from '@ngx-translate/core'; import { PlaylistStorageBackend } from '../playlist-storage-backend'; import { RuntimeCapabilitiesService } from '../runtime-capabilities.service'; import { writeServerAccessToken } from '../server-access-token'; +import { takeSlowStart } from '../server-slow-start'; import { ServerStorageStatusService } from '../server-storage-status.service'; import { SERVER_STORAGE_CONFIG } from './server-storage.config'; import { ServerStorageSession } from './server-storage-session'; @@ -370,6 +371,8 @@ describe('ServerStorageSession', () => { await session.refresh(); expect(reload).toHaveBeenCalledTimes(1); + // The reloaded page's probe waits as long as this request did. + expect(takeSlowStart()).toBe(true); }); it('sends no write before the reload checks the instance', async () => { diff --git a/libs/services/src/lib/server-storage/server-storage-session.ts b/libs/services/src/lib/server-storage/server-storage-session.ts index 3230daca4..70a623c60 100644 --- a/libs/services/src/lib/server-storage/server-storage-session.ts +++ b/libs/services/src/lib/server-storage/server-storage-session.ts @@ -19,6 +19,7 @@ import { createBrowserSessionStorage, createBrowserUserDataStorage, } from '../user-data-storage'; +import { markSlowStart } from '../server-slow-start'; import { ServerApiClient, type ServerApiError } from './server-api-client'; import { ServerPlaylistDocumentStore } from './server-playlist-document-store'; import { SERVER_STORAGE_CONFIG } from './server-storage.config'; @@ -150,6 +151,8 @@ export class ServerStorageSession { } if (!this.reloading) { this.reloading = true; + // It answers, maybe slower than the start probe waits. + markSlowStart(); this.reloadPage(); } }