Spaces:
Sleeping
Sleeping
File size: 4,696 Bytes
0dd2082 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | 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;
|