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;