All checks were successful
Build and Deploy / Build & Push (push) Successful in 3m50s
- next.config.ts: CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy - sixflags.ts: cap Retry-After at 5 min; add 15s AbortSignal.timeout() - queuetimes.ts: add 10s AbortSignal.timeout() - rcdb.ts: add 15s AbortSignal.timeout() - lib/env.ts: parseStalenessHours() guards against NaN from invalid env vars - db.ts + park-meta.ts: use parseStalenessHours() for staleness window config Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
68 lines
2.5 KiB
TypeScript
68 lines
2.5 KiB
TypeScript
/**
|
|
* park-meta.json — persisted alongside the SQLite DB in data/
|
|
*
|
|
* This file stores per-park metadata that doesn't belong in the schedule DB:
|
|
* - rcdb_id: user-supplied RCDB park ID (fills into https://rcdb.com/{id}.htm)
|
|
* - coasters: list of operating roller coaster names scraped from RCDB
|
|
* - coasters_scraped_at: ISO timestamp of last RCDB scrape
|
|
*
|
|
* discover.ts: ensures every park has a skeleton entry (rcdb_id null by default)
|
|
* scrape.ts: populates coasters[] for parks with a known rcdb_id (30-day staleness)
|
|
*/
|
|
|
|
import fs from "fs";
|
|
import path from "path";
|
|
|
|
const META_PATH = path.join(process.cwd(), "data", "park-meta.json");
|
|
|
|
export interface ParkMeta {
|
|
/** RCDB park page ID — user fills this in manually after discover creates the skeleton */
|
|
rcdb_id: number | null;
|
|
/** Operating roller coaster names scraped from RCDB */
|
|
coasters: string[];
|
|
/** ISO timestamp of when coasters was last scraped from RCDB */
|
|
coasters_scraped_at: string | null;
|
|
}
|
|
|
|
export type ParkMetaMap = Record<string, ParkMeta>;
|
|
|
|
export function readParkMeta(): ParkMetaMap {
|
|
try {
|
|
return JSON.parse(fs.readFileSync(META_PATH, "utf8")) as ParkMetaMap;
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
export function writeParkMeta(meta: ParkMetaMap): void {
|
|
fs.mkdirSync(path.dirname(META_PATH), { recursive: true });
|
|
fs.writeFileSync(META_PATH, JSON.stringify(meta, null, 2) + "\n");
|
|
}
|
|
|
|
/** Default skeleton entry for a park that has never been configured. */
|
|
export function defaultParkMeta(): ParkMeta {
|
|
return { rcdb_id: null, coasters: [], coasters_scraped_at: null };
|
|
}
|
|
|
|
const COASTER_STALE_MS = parseStalenessHours(process.env.COASTER_STALENESS_HOURS, 720) * 60 * 60 * 1000;
|
|
|
|
/** Returns true when the coaster list needs to be re-scraped from RCDB. */
|
|
export function areCoastersStale(entry: ParkMeta): boolean {
|
|
if (!entry.coasters_scraped_at) return true;
|
|
return Date.now() - new Date(entry.coasters_scraped_at).getTime() > COASTER_STALE_MS;
|
|
}
|
|
|
|
import { normalizeForMatch } from "./coaster-match";
|
|
export { normalizeForMatch as normalizeRideName } from "./coaster-match";
|
|
import { parseStalenessHours } from "./env";
|
|
|
|
/**
|
|
* Returns a Set of normalized coaster names for fast membership checks.
|
|
* Returns null when no coaster data exists for the park.
|
|
*/
|
|
export function getCoasterSet(parkId: string, meta: ParkMetaMap): Set<string> | null {
|
|
const entry = meta[parkId];
|
|
if (!entry || entry.coasters.length === 0) return null;
|
|
return new Set(entry.coasters.map(normalizeForMatch));
|
|
}
|