Files
Oxicloud/frontend/src/lib/stores/serverStatus.svelte.ts
T

68 lines
2.2 KiB
TypeScript

/**
* Reactive server-status store.
*
* Populated by the `apiFetch` wrapper, which reads the
* `x-server-status` header off every API response and calls
* `updateFromHeader(...)`. When no migration is running the header
* is absent and the store stays at its default (readonly=false, no
* migration info). See `middleware::server_status` on the server
* for the header spec.
*
* The AppShell subscribes to this store to show/hide the
* maintenance banner without polling — the state travels back to
* the client on the piggyback of whatever API request the user was
* making anyway. Zero extra network cost.
*/
/**
* JSON shape emitted in the `x-server-status` header. Optional
* `migration` field is present only while a migration is running.
*/
export interface ServerStatus {
readonly: boolean;
migration?: {
target: string;
migrated: number;
total: number;
percent: number;
};
}
const DEFAULT: ServerStatus = { readonly: false };
// Rune-based reactive state — `$state` in a `.svelte.ts` module.
let current = $state<ServerStatus>(DEFAULT);
/** Current server status. Reactively updates when apiFetch sees a new header. */
export function serverStatus(): ServerStatus {
return current;
}
/**
* Parse the raw header value and update the store. Silently
* tolerates a missing header (resets to default: nothing to
* broadcast means nothing wrong) and a malformed one (keeps the
* previous value rather than surface a parse error to users).
*
* Called by `apiFetch` after every response — see `client.ts`.
*/
export function updateFromHeader(rawHeader: string | null): void {
if (rawHeader == null) {
// No header on this response = server not in maintenance
// mode = reset the store to the default so any lingering
// banner disappears. Cheap idempotent write.
if (current.readonly || current.migration) current = DEFAULT;
return;
}
try {
const parsed = JSON.parse(rawHeader) as ServerStatus;
// Basic shape validation — server should never send a
// missing `readonly`, but be defensive.
if (typeof parsed.readonly === 'boolean') {
current = parsed;
}
} catch {
// Malformed header — keep previous state rather than churn.
}
}