mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 13:46:16 -08:00
fix(pwa): stay in server mode when a known server does not answer
A failed start-up probe used to fall back to browser mode, which showed an empty library while the backend restarted and let new playlists land in the browser. The browser now remembers the server; without an answer it starts in server mode, offline, and reloads once the server is back. Settings → Server shows the backend address instead of the page's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8714b3b445
commit
3bfd5394ef
13 files changed
+357
-69
No files matched your search
@@ -3,6 +3,16 @@ import type { IptvnatorRuntimeConfig } from '../services/runtime-config';
|
||||
|
||||
const PROBE_TIMEOUT_MS = 2_500;
|
||||
|
||||
/**
|
||||
* - `skipped`: no runtime backend URL, or Electron.
|
||||
* - `server` / `none`: the backend answered.
|
||||
* - `unreachable`: no answer, a timeout or a 5xx (nginx answers 502 while
|
||||
* the backend restarts), so the result says nothing about storage.
|
||||
*/
|
||||
export type ServerInstanceProbe =
|
||||
| { readonly kind: 'skipped' | 'none' | 'unreachable' }
|
||||
| { readonly kind: 'server'; readonly instance: ServerInstanceInfo };
|
||||
|
||||
export interface ServerInstanceProbeOptions {
|
||||
readonly runtimeConfig?: IptvnatorRuntimeConfig;
|
||||
readonly isElectron?: boolean;
|
||||
@@ -16,36 +26,42 @@ export interface ServerInstanceProbeOptions {
|
||||
*/
|
||||
export async function probeServerInstance(
|
||||
options: ServerInstanceProbeOptions = {}
|
||||
): Promise<ServerInstanceInfo | null> {
|
||||
): Promise<ServerInstanceProbe> {
|
||||
const runtimeConfig =
|
||||
options.runtimeConfig ?? globalThis.window?.__IPTVNATOR_CONFIG__;
|
||||
const isElectron = options.isElectron ?? !!globalThis.window?.electron;
|
||||
const baseUrl = runtimeConfig?.BACKEND_URL?.trim();
|
||||
if (isElectron || !baseUrl) {
|
||||
return null;
|
||||
return { kind: 'skipped' };
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
const fetchFn = options.fetchFn ?? globalThis.fetch.bind(globalThis);
|
||||
const response = await fetchFn(
|
||||
`${baseUrl.replace(/\/+$/, '')}/v1/instance`,
|
||||
{
|
||||
cache: 'no-store',
|
||||
signal:
|
||||
typeof AbortSignal.timeout === 'function'
|
||||
? AbortSignal.timeout(PROBE_TIMEOUT_MS)
|
||||
: undefined,
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
response = await fetchFn(`${baseUrl.replace(/\/+$/, '')}/v1/instance`, {
|
||||
cache: 'no-store',
|
||||
signal:
|
||||
typeof AbortSignal.timeout === 'function'
|
||||
? AbortSignal.timeout(PROBE_TIMEOUT_MS)
|
||||
: undefined,
|
||||
});
|
||||
} catch {
|
||||
return { kind: 'unreachable' };
|
||||
}
|
||||
if (response.status >= 500) {
|
||||
return { kind: 'unreachable' };
|
||||
}
|
||||
if (!response.ok) {
|
||||
// A backend from before server storage has no /v1 routes.
|
||||
return { kind: 'none' };
|
||||
}
|
||||
try {
|
||||
const body: unknown = await response.json();
|
||||
return isServerInstance(body) && body.storage === 'server'
|
||||
? body
|
||||
: null;
|
||||
? { kind: 'server', instance: body }
|
||||
: { kind: 'none' };
|
||||
} catch {
|
||||
return null;
|
||||
return { kind: 'none' };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user