import { ParkingGetterSetterRepository } from "./ParkingGetterSetterRepository"; import { IParkingStructure, IParkingStructureTimestampRecord } from "../../entities/ParkingRepositoryEntities"; import { HistoricalParkingAverageQueryResult, ParkingStructureCountOptions } from "./ParkingGetterRepository"; import { CircularQueue } from "../../types/CircularQueue"; import { PARKING_LOGGING_INTERVAL_MS } from "./ParkingRepositoryConstants"; // If every 10 minutes, two weeks of data (6x per hour * 24x per day * 7x per week * 2) export const MAX_NUM_ENTRIES = 2016; export type ParkingStructureID = string; export class InMemoryParkingRepository implements ParkingGetterSetterRepository { private dataLastAdded: Map = new Map(); private loggingIntervalMs = PARKING_LOGGING_INTERVAL_MS; constructor( private structures: Map = new Map(), private historicalData: Map> = new Map(), ) { } addOrUpdateParkingStructure = async (structure: IParkingStructure): Promise => { this.structures.set(structure.id, { ...structure }); await this.addHistoricalDataForStructure(structure); }; private addHistoricalDataForStructure = async (structure: IParkingStructure): Promise => { const now = Date.now(); const lastAdded = this.dataLastAdded.get(structure.id); if (this.shouldLogHistoricalData(lastAdded, now)) { const timestampRecord = this.createTimestampRecord(structure, now); this.ensureHistoricalDataExists(structure.id); this.addRecordToHistoricalData(structure.id, timestampRecord); this.dataLastAdded.set(structure.id, new Date(now)); } }; clearParkingStructureData = async (): Promise => { this.structures.clear(); this.historicalData.clear(); this.dataLastAdded.clear(); }; getParkingStructureById = async (id: string): Promise => { const structure = this.structures.get(id); return structure ? { ...structure } : null; }; getParkingStructures = async (): Promise => Array.from(this.structures.values()).map(structure => ({...structure})); removeParkingStructureIfExists = async (id: string): Promise => { const structure = this.structures.get(id); if (structure) { this.structures.delete(id); this.historicalData.delete(id); this.dataLastAdded.delete(id); return { ...structure }; } return null; }; getHistoricalAveragesOfParkingStructureCounts = async (id: string, options: ParkingStructureCountOptions): Promise => { const queue = this.historicalData.get(id); if (!queue || queue.size() === 0) { return []; } const records = this.extractRecordsFromQueue(queue); return this.calculateAveragesFromRecords(records, options); }; private shouldLogHistoricalData = (lastAdded: Date | undefined, currentTime: number): boolean => { return !lastAdded || (currentTime - lastAdded.getTime()) >= this.loggingIntervalMs; }; private createTimestampRecord = (structure: IParkingStructure, timestampMs: number): IParkingStructureTimestampRecord => ({ id: structure.id, spotsAvailable: structure.spotsAvailable, timestampMs, }); private ensureHistoricalDataExists = (structureId: string): void => { if (!this.historicalData.has(structureId)) { this.historicalData.set(structureId, new CircularQueue(MAX_NUM_ENTRIES)); } }; private addRecordToHistoricalData = (structureId: string, record: IParkingStructureTimestampRecord): void => { const sortingCallback = (a: IParkingStructureTimestampRecord, b: IParkingStructureTimestampRecord) => a.timestampMs - b.timestampMs; this.historicalData.get(structureId)?.appendWithSorting(record, sortingCallback); }; private extractRecordsFromQueue = (queue: CircularQueue): IParkingStructureTimestampRecord[] => { const records: IParkingStructureTimestampRecord[] = []; for (let i = 0; i < queue.size(); i++) { const record = queue.get(i); if (record) { records.push(record); } } return records; }; private calculateAveragesFromRecords = ( records: IParkingStructureTimestampRecord[], options: ParkingStructureCountOptions ): HistoricalParkingAverageQueryResult[] => { const results: HistoricalParkingAverageQueryResult[] = []; const { startUnixEpochMs, endUnixEpochMs, intervalMs } = options; let currentIntervalStart = startUnixEpochMs; while (currentIntervalStart < endUnixEpochMs) { const currentIntervalEnd = Math.min(currentIntervalStart + intervalMs, endUnixEpochMs); const recordsInInterval = this.getRecordsInTimeRange(records, currentIntervalStart, currentIntervalEnd); if (recordsInInterval.length > 0) { const averageResult = this.calculateAverageForInterval(currentIntervalStart, currentIntervalEnd, recordsInInterval); results.push(averageResult); } currentIntervalStart = currentIntervalEnd; } return results; }; private getRecordsInTimeRange = ( records: IParkingStructureTimestampRecord[], startMs: number, endMs: number ): IParkingStructureTimestampRecord[] => { return records.filter(record => record.timestampMs >= startMs && record.timestampMs < endMs ); }; private calculateAverageForInterval = ( fromMs: number, toMs: number, records: IParkingStructureTimestampRecord[] ): HistoricalParkingAverageQueryResult => { const totalSpotsAvailable = records.reduce((sum, record) => sum + record.spotsAvailable, 0); const averageSpotsAvailable = totalSpotsAvailable / records.length; return { fromUnixEpochMs: fromMs, toUnixEpochMs: toMs, averageSpotsAvailable }; }; setLoggingInterval = (intervalMs: number): void => { this.loggingIntervalMs = intervalMs; }; }