Spaces:
Running
Running
| const logger = require('../utils/logger'); | |
| const cache = require('./cache.service'); | |
| const { paginate } = require('../utils/response'); | |
| /** | |
| * Provider Orchestrator | |
| * | |
| * Interview note: This implements the Chain of Responsibility pattern. | |
| * Providers are sorted by priority (lower = tried first). If a provider | |
| * fails, the orchestrator falls to the next one. Combined with caching, | |
| * this gives us: cache → provider1 → provider2 → ... → mock fallback. | |
| */ | |
| class ProviderOrchestrator { | |
| constructor() { | |
| this.providers = []; | |
| } | |
| register(provider) { | |
| this.providers.push(provider); | |
| this.providers.sort((a, b) => a.priority - b.priority); | |
| logger.info(`Provider registered: ${provider.name}`, { | |
| priority: provider.priority, | |
| total: this.providers.length, | |
| }); | |
| } | |
| getProviders() { | |
| return this.providers.map(p => p.toJSON()); | |
| } | |
| async search(intent, options = {}) { | |
| // 1. Check cache first | |
| const cached = cache.get(intent); | |
| if (cached) { | |
| const page = parseInt(options.page, 10) || 1; | |
| const limit = parseInt(options.limit, 10) || 10; | |
| const paginated = paginate(cached.results, page, limit); | |
| return { | |
| ...paginated, | |
| meta: { | |
| ...cached.meta, | |
| cached: true, | |
| source: `${cached.meta.source} (cached)`, | |
| scrapedAt: cached.meta.scrapedAt, | |
| }, | |
| }; | |
| } | |
| // 2. Try providers in priority order | |
| const enabledProviders = this.providers.filter(p => p.enabled); | |
| for (const provider of enabledProviders) { | |
| try { | |
| logger.info(`Trying provider: ${provider.name}`); | |
| const data = await this._executeWithTimeout(provider, intent); | |
| if (!data || !data.results) { | |
| throw new Error('Provider returned invalid data structure'); | |
| } | |
| // If no results, try the next provider | |
| if (data.results.length === 0) { | |
| logger.info(`Provider ${provider.name} returned 0 results. Falling back to next...`); | |
| continue; | |
| } | |
| // Cache successful non-empty results | |
| data.meta.scrapedAt = new Date().toISOString(); | |
| cache.set(intent, data); | |
| // Paginate | |
| const page = parseInt(options.page, 10) || 1; | |
| const limit = parseInt(options.limit, 10) || 10; | |
| const paginated = paginate(data.results, page, limit); | |
| return { | |
| ...paginated, | |
| meta: { | |
| ...data.meta, | |
| cached: false, | |
| source: provider.name, | |
| scrapedAt: data.meta.scrapedAt, | |
| }, | |
| }; | |
| } catch (err) { | |
| logger.warn(`Provider ${provider.name} failed`, { error: err.message }); | |
| continue; | |
| } | |
| } | |
| // If all providers actually crashed/failed, return graceful empty state | |
| return { | |
| results: [], | |
| limit: parseInt(options.limit, 10) || 10, | |
| page: parseInt(options.page, 10) || 1, | |
| totalPages: 0, | |
| meta: { source: 'System', message: 'All data providers failed.' } | |
| }; | |
| } | |
| async _executeWithTimeout(provider, intent) { | |
| return Promise.race([ | |
| provider.search(intent), | |
| new Promise((_, reject) => | |
| setTimeout(() => reject(new Error(`Provider ${provider.name} timed out`)), provider.timeout) | |
| ), | |
| ]); | |
| } | |
| async healthCheck() { | |
| const results = {}; | |
| for (const provider of this.providers) { | |
| try { | |
| results[provider.name] = await provider.healthCheck(); | |
| } catch (err) { | |
| results[provider.name] = { healthy: false, error: err.message }; | |
| } | |
| } | |
| results.cache = cache.getStats(); | |
| return results; | |
| } | |
| } | |
| // Singleton orchestrator with providers registered | |
| const DatabaseProvider = require('../providers/DatabaseProvider'); | |
| const OSMProvider = require('../providers/OSMProvider'); | |
| const orchestrator = new ProviderOrchestrator(); | |
| // Priority 1: Custom Scraped Database entries (most specific) | |
| orchestrator.register(new DatabaseProvider()); | |
| // Priority 2: OpenStreetMap (Global fallback for real-time discovery) | |
| orchestrator.register(new OSMProvider()); | |
| module.exports = orchestrator; | |