import { readFile, readdir } from 'fs/promises';
import { join } from 'path';
import { StateData, StateDataSchema, STATE_NAME_MAP } from '../types/stateData.js';
class LRUCache<T> {
private cache: Map<string, { value: T; timestamp: number }>;
private maxSize: number;
private ttl: number;
constructor(maxSize: number = 100, ttlSeconds: number = 3600) {
this.cache = new Map();
this.maxSize = maxSize;
this.ttl = ttlSeconds * 1000;
}
get(key: string): T | null {
const item = this.cache.get(key);
if (!item) return null;
if (Date.now() - item.timestamp > this.ttl) {
this.cache.delete(key);
return null;
}
this.cache.delete(key);
this.cache.set(key, item);
return item.value;
}
set(key: string, value: T): void {
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
if (firstKey) {
this.cache.delete(firstKey);
}
}
this.cache.set(key, { value, timestamp: Date.now() });
}
clear(): void {
this.cache.clear();
}
size(): number {
return this.cache.size;
}
has(key: string): boolean {
return this.cache.has(key) && this.get(key) !== null;
}
}
export class DataLoader {
private dataPath: string;
private cache: LRUCache<StateData>;
private allStatesCache: StateData[] | null = null;
private allStatesCacheTimestamp: number = 0;
private cacheTTL: number;
constructor(dataPath: string, cacheTTL: number = 3600) {
this.dataPath = dataPath;
this.cacheTTL = cacheTTL * 1000;
this.cache = new LRUCache<StateData>(200, cacheTTL);
}
private normalizeStateName(input: string): string {
const normalized = input.toLowerCase().trim();
if (STATE_NAME_MAP[normalized]) {
return STATE_NAME_MAP[normalized];
}
if (normalized.includes('-')) {
return normalized;
}
return normalized.replace(/\s+/g, '-');
}
async loadStateData(stateName: string): Promise<StateData> {
const normalizedName = this.normalizeStateName(stateName);
const cached = this.cache.get(normalizedName);
if (cached) {
return cached;
}
const filePath = join(this.dataPath, `${normalizedName}.json`);
try {
const fileContent = await readFile(filePath, 'utf-8');
const rawData = JSON.parse(fileContent);
const validatedData = StateDataSchema.parse(rawData);
this.cache.set(normalizedName, validatedData);
return validatedData;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
throw new Error(`State data not found: ${stateName}. Please check the state name format (e.g., 'new-jersey', 'pennsylvania').`);
}
throw new Error(`Failed to load state data for ${stateName}: ${(error as Error).message}`);
}
}
async loadAllStates(): Promise<StateData[]> {
if (this.allStatesCache && Date.now() - this.allStatesCacheTimestamp < this.cacheTTL) {
return this.allStatesCache;
}
try {
const files = await readdir(this.dataPath);
const jsonFiles = files.filter(file => file.endsWith('.json'));
const statePromises = jsonFiles.map(async (file) => {
const stateName = file.replace('.json', '');
return this.loadStateData(stateName);
});
const states = await Promise.all(statePromises);
this.allStatesCache = states;
this.allStatesCacheTimestamp = Date.now();
return states;
} catch (error) {
throw new Error(`Failed to load all states: ${(error as Error).message}`);
}
}
async getAvailableStates(): Promise<string[]> {
try {
const files = await readdir(this.dataPath);
return files
.filter(file => file.endsWith('.json'))
.map(file => file.replace('.json', ''))
.sort();
} catch (error) {
throw new Error(`Failed to get available states: ${(error as Error).message}`);
}
}
async stateExists(stateName: string): Promise<boolean> {
const normalizedName = this.normalizeStateName(stateName);
const availableStates = await this.getAvailableStates();
return availableStates.includes(normalizedName);
}
clearCache(): void {
this.cache.clear();
this.allStatesCache = null;
this.allStatesCacheTimestamp = 0;
}
getCacheStats(): {
size: number;
maxSize: number;
ttlSeconds: number;
allStatesCached: boolean;
} {
return {
size: this.cache.size(),
maxSize: 200,
ttlSeconds: this.cacheTTL / 1000,
allStatesCached: this.allStatesCache !== null
};
}
async loadMultipleStates(stateNames: string[]): Promise<StateData[]> {
const promises = stateNames.map(name => this.loadStateData(name));
return Promise.all(promises);
}
}