Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
+140
-22
@@ -1,26 +1,122 @@
|
||||
import type { MetricsData } from '$lib/metrics.js';
|
||||
import { validTimeZone } from '$lib/time-zone.js';
|
||||
|
||||
const record = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
const numeric = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value);
|
||||
const record = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
const numeric = (value: unknown): value is number =>
|
||||
typeof value === 'number' && Number.isFinite(value);
|
||||
const timestamp = (value: unknown) => value === null || numeric(value);
|
||||
const totals = (value: unknown) => record(value) && ['observed', 'active', 'idle', 'unknown', 'playerHours'].every(key => numeric(value[key]) && value[key] >= 0);
|
||||
const totals = (value: unknown) =>
|
||||
record(value) &&
|
||||
['observed', 'active', 'idle', 'unknown', 'playerHours'].every(
|
||||
(key) => numeric(value[key]) && value[key] >= 0
|
||||
);
|
||||
|
||||
function validReport(value: unknown): value is MetricsData {
|
||||
if (!record(value) || ![1, 7, 30].includes(value.days as number) || !numeric(value.now) || !numeric(value.since) || !totals(value.summary)) return false;
|
||||
if (!record(value.scope) || !validTimeZone(value.scope.timeZone) || ![value.scope.game, value.scope.map].every(item => item === null || typeof item === 'string')) return false;
|
||||
if (!record(value.scopeOptions) || !Array.isArray(value.scopeOptions.games) || !value.scopeOptions.games.every(item => typeof item === 'string') ||
|
||||
!Array.isArray(value.scopeOptions.maps) || !value.scopeOptions.maps.every(item => record(item) && typeof item.game === 'string' && typeof item.map === 'string')) return false;
|
||||
if (!numeric(value.registeredServerCount) || !['firstRetainedSampleAt', 'latestSampleAt', 'scopeFirstSampleAt', 'scopeLatestSampleAt'].every(key => timestamp(value[key]))) return false;
|
||||
if (!Array.isArray(value.points) || !value.points.every(item => record(item) && totals(item) && numeric(item.at) && numeric(item.startAt) && numeric(item.endAt) && typeof item.partial === 'boolean')) return false;
|
||||
if (!Array.isArray(value.maps) || !value.maps.every(item => record(item) && totals(item) && typeof item.game === 'string' && typeof item.map === 'string' && numeric(item.full) && numeric(item.capacityObserved))) return false;
|
||||
if (!Array.isArray(value.servers) || !value.servers.every(item => record(item) && totals(item) && typeof item.server === 'string' && typeof item.currentRegistered === 'boolean')) return false;
|
||||
if (!record(value.previous) || !totals(value.previous.summary) || !numeric(value.previous.since) || !numeric(value.previous.until) ||
|
||||
!timestamp(value.previous.firstSampleAt) || !timestamp(value.previous.lastSampleAt) || typeof value.previous.partialHistory !== 'boolean') return false;
|
||||
if (!record(value.heatmap) || !validTimeZone(value.heatmap.timeZone) || !Array.isArray(value.heatmap.cells) ||
|
||||
!value.heatmap.cells.every(item => record(item) && totals(item) && numeric(item.weekday) && numeric(item.hour) && numeric(item.distinctDates))) return false;
|
||||
return record(value.pooledPressure) && (value.pooledPressure.value === null || numeric(value.pooledPressure.value)) &&
|
||||
['fullMapHours', 'knownMapHours', 'coveredMaps', 'totalMaps'].every(key => numeric((value.pooledPressure as Record<string, unknown>)[key]));
|
||||
if (
|
||||
!record(value) ||
|
||||
![1, 7, 30].includes(value.days as number) ||
|
||||
!numeric(value.now) ||
|
||||
!numeric(value.since) ||
|
||||
!totals(value.summary)
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
!record(value.scope) ||
|
||||
!validTimeZone(value.scope.timeZone) ||
|
||||
![value.scope.game, value.scope.map].every(
|
||||
(item) => item === null || typeof item === 'string'
|
||||
)
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
!record(value.scopeOptions) ||
|
||||
!Array.isArray(value.scopeOptions.games) ||
|
||||
!value.scopeOptions.games.every((item) => typeof item === 'string') ||
|
||||
!Array.isArray(value.scopeOptions.maps) ||
|
||||
!value.scopeOptions.maps.every(
|
||||
(item) => record(item) && typeof item.game === 'string' && typeof item.map === 'string'
|
||||
)
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
!numeric(value.registeredServerCount) ||
|
||||
![
|
||||
'firstRetainedSampleAt',
|
||||
'latestSampleAt',
|
||||
'scopeFirstSampleAt',
|
||||
'scopeLatestSampleAt'
|
||||
].every((key) => timestamp(value[key]))
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
!Array.isArray(value.points) ||
|
||||
!value.points.every(
|
||||
(item) =>
|
||||
record(item) &&
|
||||
totals(item) &&
|
||||
numeric(item.at) &&
|
||||
numeric(item.startAt) &&
|
||||
numeric(item.endAt) &&
|
||||
typeof item.partial === 'boolean'
|
||||
)
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
!Array.isArray(value.maps) ||
|
||||
!value.maps.every(
|
||||
(item) =>
|
||||
record(item) &&
|
||||
totals(item) &&
|
||||
typeof item.game === 'string' &&
|
||||
typeof item.map === 'string' &&
|
||||
numeric(item.full) &&
|
||||
numeric(item.capacityObserved)
|
||||
)
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
!Array.isArray(value.servers) ||
|
||||
!value.servers.every(
|
||||
(item) =>
|
||||
record(item) &&
|
||||
totals(item) &&
|
||||
typeof item.server === 'string' &&
|
||||
typeof item.currentRegistered === 'boolean'
|
||||
)
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
!record(value.previous) ||
|
||||
!totals(value.previous.summary) ||
|
||||
!numeric(value.previous.since) ||
|
||||
!numeric(value.previous.until) ||
|
||||
!timestamp(value.previous.firstSampleAt) ||
|
||||
!timestamp(value.previous.lastSampleAt) ||
|
||||
typeof value.previous.partialHistory !== 'boolean'
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
!record(value.heatmap) ||
|
||||
!validTimeZone(value.heatmap.timeZone) ||
|
||||
!Array.isArray(value.heatmap.cells) ||
|
||||
!value.heatmap.cells.every(
|
||||
(item) =>
|
||||
record(item) &&
|
||||
totals(item) &&
|
||||
numeric(item.weekday) &&
|
||||
numeric(item.hour) &&
|
||||
numeric(item.distinctDates)
|
||||
)
|
||||
)
|
||||
return false;
|
||||
return (
|
||||
record(value.pooledPressure) &&
|
||||
(value.pooledPressure.value === null || numeric(value.pooledPressure.value)) &&
|
||||
['fullMapHours', 'knownMapHours', 'coveredMaps', 'totalMaps'].every((key) =>
|
||||
numeric((value.pooledPressure as Record<string, unknown>)[key])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
interface Options {
|
||||
@@ -36,7 +132,12 @@ interface Options {
|
||||
export function createMetricsRefresh(options: Options) {
|
||||
const request = options.fetcher ?? fetch;
|
||||
let generation = 0;
|
||||
let pending: { query: string; controller: AbortController; promise: Promise<void>; timer?: ReturnType<typeof setTimeout> } | null = null;
|
||||
let pending: {
|
||||
query: string;
|
||||
controller: AbortController;
|
||||
promise: Promise<void>;
|
||||
timer?: ReturnType<typeof setTimeout>;
|
||||
} | null = null;
|
||||
|
||||
function cancel() {
|
||||
generation++;
|
||||
@@ -58,12 +159,23 @@ export function createMetricsRefresh(options: Options) {
|
||||
const current = () => generation === id && options.getQuery() === query;
|
||||
options.onLoading(true);
|
||||
options.onError(null);
|
||||
const entry = { query, controller, promise: Promise.resolve(), timer: undefined as ReturnType<typeof setTimeout> | undefined };
|
||||
const entry = {
|
||||
query,
|
||||
controller,
|
||||
promise: Promise.resolve(),
|
||||
timer: undefined as ReturnType<typeof setTimeout> | undefined
|
||||
};
|
||||
pending = entry;
|
||||
entry.timer = setTimeout(() => { timedOut = true; controller.abort(); }, options.timeoutMs ?? 15_000);
|
||||
entry.timer = setTimeout(() => {
|
||||
timedOut = true;
|
||||
controller.abort();
|
||||
}, options.timeoutMs ?? 15_000);
|
||||
entry.promise = (async () => {
|
||||
try {
|
||||
const response = await request(`/api/metrics?${query}`, { signal: controller.signal, cache: 'no-store' });
|
||||
const response = await request(`/api/metrics?${query}`, {
|
||||
signal: controller.signal,
|
||||
cache: 'no-store'
|
||||
});
|
||||
if (!response.ok) throw new Error(`Report refresh failed (${response.status}).`);
|
||||
const report: unknown = await response.json();
|
||||
if (!validReport(report)) {
|
||||
@@ -72,7 +184,13 @@ export function createMetricsRefresh(options: Options) {
|
||||
if (current()) options.onData(report);
|
||||
} catch (error) {
|
||||
if (current() && (!controller.signal.aborted || timedOut)) {
|
||||
options.onError(timedOut ? 'Report refresh timed out. Try again.' : error instanceof Error ? error.message : 'Unable to refresh metrics. Try again.');
|
||||
options.onError(
|
||||
timedOut
|
||||
? 'Report refresh timed out. Try again.'
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: 'Unable to refresh metrics. Try again.'
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(entry.timer);
|
||||
|
||||
Reference in New Issue
Block a user