import type { MapContainer } from '@/components/MapContainer'; import type { MapLayers } from '@/types'; import { fetchEarthquakes } from '@/services/earthquakes'; import { fetchNaturalEvents } from '@/services/eonet'; import { fetchProtestEvents } from '@/services/unrest'; import { fetchWeatherAlerts } from '@/services/weather'; import { startSmartPollLoop, type SmartPollLoopHandle } from '@/services/smart-poll-loop'; import type { EmbedLayerId } from './embed-url'; import { ConflictServiceClient } from '@/services/generated-rpc-clients'; const REFRESH_MS = 10 * 60 * 1000; const CONFLICT_WINDOW_MS = 30 * 24 * 60 * 60 * 1000; const conflictClient = new ConflictServiceClient('', { fetch: (...args) => globalThis.fetch(...args) }); const STATIC_LAYER_READY_BY_EMBED_ID: Partial> = { cables: 'cables', pipelines: 'pipelines', waterways: 'waterways', tradeRoutes: 'tradeRoutes', economic: 'economic', stockExchanges: 'stockExchanges', financialCenters: 'financialCenters', centralBanks: 'centralBanks', commodityHubs: 'commodityHubs', gulfInvestments: 'gulfInvestments', }; export class EmbedDataLoader { private refreshLoop: SmartPollLoopHandle | null = null; constructor( private readonly map: MapContainer, private readonly activeLayerIds: readonly EmbedLayerId[], ) {} async start(): Promise { await this.loadOnce(); this.refreshLoop = startSmartPollLoop(() => this.loadOnce(), { intervalMs: REFRESH_MS, pauseWhenHidden: true, refreshOnVisible: true, runImmediately: false, }); } destroy(): void { if (this.refreshLoop !== null) { this.refreshLoop.stop(); this.refreshLoop = null; } } async loadOnce(): Promise { await Promise.all(this.activeLayerIds.map((id) => this.loadLayer(id))); } private async loadLayer(id: EmbedLayerId): Promise { switch (id) { case 'conflicts': await this.loadConflicts(); return; case 'earthquakes': await this.loadEarthquakes(); return; case 'protests': await this.loadProtests(); return; case 'weather': await this.loadWeather(); return; default: this.markStaticLayerReady(id); return; } } private markStaticLayerReady(id: EmbedLayerId): void { const layer = STATIC_LAYER_READY_BY_EMBED_ID[id]; if (layer) this.map.setLayerReady(layer, true); } private async loadConflicts(): Promise { if (!this.map.supportsLiveConflictEvents()) { this.map.setLayerReady('conflicts', true); return; } await this.withLayerState('conflicts', async () => { const end = Date.now(); const start = end - CONFLICT_WINDOW_MS; const data = await conflictClient.listAcledEvents({ country: '', start, end, pageSize: 0, cursor: '' }); this.map.setConflictEvents(data.events); return data.events.length > 0; }); } private async loadEarthquakes(): Promise { await this.withLayerState('natural', async () => { const [earthquakesResult, naturalEventsResult] = await Promise.allSettled([ fetchEarthquakes(), fetchNaturalEvents(30), ]); if (earthquakesResult.status === 'fulfilled') { this.map.setEarthquakes(earthquakesResult.value); } if (naturalEventsResult.status === 'fulfilled') { this.map.setNaturalEvents(naturalEventsResult.value); } return earthquakesResult.status === 'fulfilled' || naturalEventsResult.status === 'fulfilled'; }); } private async loadProtests(): Promise { await this.withLayerState('protests', async () => { const data = await fetchProtestEvents(); this.map.setProtests(data.events); return true; }); } private async loadWeather(): Promise { await this.withLayerState('weather', async () => { const alerts = await fetchWeatherAlerts(); this.map.setWeatherAlerts(alerts); return true; }); } private async withLayerState(layer: keyof MapLayers, load: () => Promise): Promise { this.map.setLayerLoading(layer, true); try { const hasData = await load(); this.map.setLayerReady(layer, hasData); } catch (error) { console.warn(`[embed] Failed to load ${layer}:`, error); this.map.setLayerReady(layer, false); } finally { this.map.setLayerLoading(layer, false); } } }