78 lines
4.0 KiB
TypeScript
78 lines
4.0 KiB
TypeScript
import { derived, writable } from 'svelte/store';
|
|
import { browser } from '$app/environment';
|
|
import { statusByServer, statusReceivedAt, syncServerTime, matchEventsByServer } from './servers.js';
|
|
import { type FleetServer, type FleetOperation } from '$lib/fleet-overview.js';
|
|
|
|
export const fleet = writable<{ servers: FleetServer[]; operations: FleetOperation[]; receivedAt: number }>({
|
|
servers: [], operations: [], receivedAt: 0
|
|
});
|
|
export const fleetError = writable('');
|
|
export const fleetRefreshing = writable(false);
|
|
export const fleetIntegrations = writable<{ rcon: boolean; komodo: boolean; rconTimeoutMs: number; defaultRconHost: string } | null>(null);
|
|
|
|
export const fleetServers = derived([fleet, statusByServer, statusReceivedAt, matchEventsByServer], ([snapshot, live, received, matches]) =>
|
|
snapshot.servers.map(server => {
|
|
const status = received > snapshot.receivedAt ? live.get(server.name) : undefined;
|
|
const current = status ? { ...server, last_players: status.players, last_map: status.map,
|
|
last_gametype: status.gametype, last_seen_at: status.lastSeenAt, last_error: status.lastError,
|
|
roster: status.roster, health: status.health, endpoint: status.endpoint, replySnippet: status.replySnippet, active_since: status.activeSince,
|
|
inactive_since: status.inactiveSince, game_started_at: status.gameStartedAt,
|
|
game_ended_at: status.gameEndedAt } : server;
|
|
const match = matches.get(server.name);
|
|
// Apply lifecycle events immediately, until a newer status observes them
|
|
// or confirms that the lobby has emptied.
|
|
if (!match || Date.parse(match.receivedAt) <= Date.parse(current.last_seen_at ?? '')
|
|
|| match.receivedAt < (current.game_started_at ?? '')
|
|
|| match.receivedAt < (current.game_ended_at ?? '')) return current;
|
|
return { ...current, game_started_at: match.event === 'game_start' ? match.receivedAt : current.game_started_at,
|
|
game_ended_at: match.event === 'game_end' ? match.receivedAt : null };
|
|
}));
|
|
|
|
let inFlight: Promise<void> | null = null;
|
|
export function refreshFleet(): Promise<void> {
|
|
if (!browser) return Promise.resolve();
|
|
if (inFlight) return inFlight;
|
|
inFlight = (async () => {
|
|
fleetRefreshing.set(true);
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), 12_000);
|
|
const requestedAt = Date.now();
|
|
try {
|
|
const responses = await Promise.all([
|
|
fetch('/api/servers', { signal: controller.signal }),
|
|
fetch('/api/fleet', { signal: controller.signal })
|
|
]);
|
|
if (responses.some(response => !response.ok)) throw new Error('Could not refresh Fleet. Retrying automatically.');
|
|
const [servers, operations] = await Promise.all(responses.map(response => response.json()));
|
|
syncServerTime(servers.serverNow);
|
|
fleetError.set('');
|
|
fleetIntegrations.set(servers.integrations);
|
|
fleet.set({ servers: servers.servers, operations: operations.operations, receivedAt: requestedAt });
|
|
} catch (error) {
|
|
fleetError.set(error instanceof Error && error.name !== 'AbortError'
|
|
? error.message : 'Fleet refresh timed out. Retrying automatically.');
|
|
} finally {
|
|
clearTimeout(timeout);
|
|
fleetRefreshing.set(false);
|
|
}
|
|
})().finally(() => { inFlight = null; });
|
|
return inFlight;
|
|
}
|
|
|
|
let stop: (() => void) | null = null;
|
|
export function startFleetMonitor(): () => void {
|
|
if (!browser || stop) return () => {};
|
|
void refreshFleet();
|
|
const interval = setInterval(() => {
|
|
if (document.visibilityState !== 'hidden') void refreshFleet();
|
|
}, 10_000);
|
|
const onFocus = () => { if (document.visibilityState === 'visible') void refreshFleet(); };
|
|
document.addEventListener('visibilitychange', onFocus);
|
|
stop = () => {
|
|
clearInterval(interval);
|
|
document.removeEventListener('visibilitychange', onFocus);
|
|
stop = null;
|
|
};
|
|
return stop;
|
|
}
|