@mrxkun/mcfast-mcp 4.0.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Code Indexer
3
+ * Tổng hợp việc parse file, extract facts, chunk và embed
4
+ */
5
+
6
+ import { parse } from '../../strategies/tree-sitter/index.js';
7
+ import { detectLanguage } from '../../strategies/syntax-validator.js';
8
+ import { Chunker } from './chunker.js';
9
+ import { Embedder } from './embedder.js';
10
+ import crypto from 'crypto';
11
+
12
+ export class CodeIndexer {
13
+ constructor(options = {}) {
14
+ this.chunker = new Chunker(options.chunker);
15
+ this.embedder = new Embedder(options.embedder);
16
+ }
17
+
18
+ async indexFile(filePath, content) {
19
+ const startTime = Date.now();
20
+
21
+ const language = detectLanguage(filePath);
22
+ const contentHash = crypto.createHash('md5').update(content).digest('hex');
23
+
24
+ const ast = await this.parseAST(content, filePath, language);
25
+ const facts = ast ? await this.extractFacts(ast, filePath, language) : [];
26
+ const chunks = this.chunker.chunk(content, { filePath, language });
27
+ const embeddings = await this.generateEmbeddings(chunks, language);
28
+
29
+ const duration = Date.now() - startTime;
30
+
31
+ return {
32
+ file: {
33
+ id: this.generateFileId(filePath),
34
+ path: filePath,
35
+ content_hash: contentHash,
36
+ last_modified: Date.now(),
37
+ language,
38
+ line_count: content.split('\n').length
39
+ },
40
+ facts,
41
+ chunks,
42
+ embeddings,
43
+ duration
44
+ };
45
+ }
46
+
47
+ async parseAST(content, filePath, language) {
48
+ try {
49
+ return await parse(content, filePath);
50
+ } catch (error) {
51
+ console.warn(`[Indexer] Failed to parse ${filePath}:`, error.message);
52
+ return null;
53
+ }
54
+ }
55
+
56
+ async extractFacts(ast, filePath, language) {
57
+ const facts = [];
58
+ const fileId = this.generateFileId(filePath);
59
+
60
+ try {
61
+ const { QUERIES } = await import('../../strategies/tree-sitter/queries.js');
62
+ const queries = QUERIES[language];
63
+
64
+ if (queries?.definitions) {
65
+ const captures = queries.definitions.captures(ast.rootNode);
66
+
67
+ for (const capture of captures) {
68
+ if (capture.name === 'name') {
69
+ facts.push({
70
+ id: this.generateFactId(fileId, capture.node.text, 'function'),
71
+ file_id: fileId,
72
+ type: 'function',
73
+ name: capture.node.text,
74
+ line_start: capture.node.startPosition.row + 1,
75
+ line_end: capture.node.endPosition.row + 1,
76
+ signature: '',
77
+ exported: false,
78
+ confidence: 1.0
79
+ });
80
+ }
81
+ }
82
+ }
83
+ } catch (error) {
84
+ console.warn(`[Indexer] Failed to extract facts:`, error.message);
85
+ }
86
+
87
+ return facts;
88
+ }
89
+
90
+ async generateEmbeddings(chunks, language = 'javascript') {
91
+ const embeddings = [];
92
+
93
+ try {
94
+ for (const chunk of chunks) {
95
+ const vector = await this.embedder.embedCode(chunk.content, language);
96
+ embeddings.push({
97
+ chunk_id: chunk.id,
98
+ embedding: Buffer.from(new Float32Array(vector).buffer),
99
+ model: 'simple-embedder'
100
+ });
101
+ }
102
+ } catch (error) {
103
+ console.error('[Indexer] Failed to generate embeddings:', error.message);
104
+ }
105
+
106
+ return embeddings;
107
+ }
108
+
109
+ generateFileId(filePath) {
110
+ return crypto.createHash('md5').update(filePath).digest('hex').substring(0, 16);
111
+ }
112
+
113
+ generateFactId(fileId, name, type) {
114
+ return crypto.createHash('md5').update(`${fileId}:${name}:${type}`).digest('hex').substring(0, 16);
115
+ }
116
+ }
117
+
118
+ export default CodeIndexer;
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Simple Embedding Generator
3
+ * KHÔNG dùng LLM - Chỉ dùng thuật toán truyền thống
4
+ * Methods: TF-IDF, Bag-of-Words, Code-specific features
5
+ */
6
+
7
+ export class SimpleEmbedder {
8
+ constructor(options = {}) {
9
+ this.dimension = options.dimension || 512;
10
+ this.vocabulary = new Map(); // word -> index
11
+ this.idf = new Map(); // word -> idf score
12
+ this.documentCount = 0;
13
+ }
14
+
15
+ /**
16
+ * TF-IDF Embedding
17
+ * Phương pháp truyền thống dùng trong search engines
18
+ */
19
+ embed(text) {
20
+ const vector = new Array(this.dimension).fill(0);
21
+
22
+ // Tokenize code
23
+ const tokens = this.tokenize(text);
24
+
25
+ // Calculate TF (Term Frequency)
26
+ const tf = new Map();
27
+ tokens.forEach(token => {
28
+ tf.set(token, (tf.get(token) || 0) + 1);
29
+ });
30
+
31
+ // Normalize TF
32
+ const maxTf = Math.max(...tf.values());
33
+ tf.forEach((count, token) => {
34
+ tf.set(token, count / maxTf);
35
+ });
36
+
37
+ // Create embedding vector
38
+ tf.forEach((freq, token) => {
39
+ const index = this.getTokenIndex(token);
40
+ const idf = this.idf.get(token) || 1;
41
+ vector[index % this.dimension] = freq * idf;
42
+ });
43
+
44
+ // Normalize vector
45
+ return this.normalize(vector);
46
+ }
47
+
48
+ /**
49
+ * Code-specific embedding
50
+ * Tập trung vào đặc trưng của code
51
+ */
52
+ embedCode(code, language = 'javascript') {
53
+ const vector = new Array(this.dimension).fill(0);
54
+
55
+ // 1. Tokenize và tách từ khóa
56
+ const tokens = this.tokenize(code);
57
+
58
+ // 2. Trích xuất đặc trưng code
59
+ const features = this.extractCodeFeatures(code, language);
60
+
61
+ // 3. Kết hợp features vào vector
62
+ let index = 0;
63
+
64
+ // Function names (vị trí 0-100)
65
+ features.functions.forEach((func, i) => {
66
+ const hash = this.hashString(func) % 100;
67
+ vector[hash] = 1.0;
68
+ });
69
+
70
+ // Variable names (vị trí 100-200)
71
+ features.variables.forEach((variable, i) => {
72
+ const hash = 100 + (this.hashString(variable) % 100);
73
+ vector[hash] = 0.8;
74
+ });
75
+
76
+ // Class names (vị trí 200-300)
77
+ features.classes.forEach((cls, i) => {
78
+ const hash = 200 + (this.hashString(cls) % 100);
79
+ vector[hash] = 1.0;
80
+ });
81
+
82
+ // Keywords (vị trí 300-400)
83
+ const keywords = this.getKeywords(tokens, language);
84
+ keywords.forEach((keyword, i) => {
85
+ const hash = 300 + (this.hashString(keyword) % 100);
86
+ vector[hash] = 0.6;
87
+ });
88
+
89
+ // Import/Export patterns (vị trí 400-500)
90
+ features.imports.forEach((imp, i) => {
91
+ const hash = 400 + (this.hashString(imp) % 100);
92
+ vector[hash] = 0.9;
93
+ });
94
+
95
+ // Code structure (vị trí 500-512)
96
+ vector[500] = features.hasAsync ? 1.0 : 0;
97
+ vector[501] = features.hasClass ? 1.0 : 0;
98
+ vector[502] = features.hasExport ? 1.0 : 0;
99
+ vector[503] = features.lineCount / 1000; // Normalized
100
+
101
+ return this.normalize(vector);
102
+ }
103
+
104
+ /**
105
+ * Tokenize text/code
106
+ */
107
+ tokenize(text) {
108
+ // Tách từ theo camelCase, snake_case, và ký tự đặc biệt
109
+ return text
110
+ .replace(/([a-z])([A-Z])/g, '$1 $2') // camelCase → camel Case
111
+ .replace(/_/g, ' ') // snake_case → snake case
112
+ .toLowerCase()
113
+ .match(/[a-z]+/g) || [];
114
+ }
115
+
116
+ /**
117
+ * Trích xuất đặc trưng từ code
118
+ */
119
+ extractCodeFeatures(code, language) {
120
+ const features = {
121
+ functions: [],
122
+ variables: [],
123
+ classes: [],
124
+ imports: [],
125
+ hasAsync: /\basync\b/.test(code),
126
+ hasClass: /\bclass\b/.test(code),
127
+ hasExport: /\bexport\b/.test(code),
128
+ lineCount: code.split('\n').length
129
+ };
130
+
131
+ // Extract function names
132
+ const funcMatches = code.matchAll(/(?:function|const|let|var)\s+(\w+)\s*[=\(]/g);
133
+ for (const match of funcMatches) {
134
+ features.functions.push(match[1]);
135
+ }
136
+
137
+ // Extract arrow functions
138
+ const arrowMatches = code.matchAll(/(?:const|let|var)\s+(\w+)\s*=\s*\(?.*\)?\s*=>/g);
139
+ for (const match of arrowMatches) {
140
+ features.functions.push(match[1]);
141
+ }
142
+
143
+ // Extract class names
144
+ const classMatches = code.matchAll(/class\s+(\w+)/g);
145
+ for (const match of classMatches) {
146
+ features.classes.push(match[1]);
147
+ }
148
+
149
+ // Extract imports
150
+ const importMatches = code.matchAll(/import\s+(?:{?\s*([^{}]+)\s*}?\s+from\s+)?['"]([^'"]+)['"]/g);
151
+ for (const match of importMatches) {
152
+ if (match[1]) {
153
+ features.imports.push(...match[1].split(',').map(s => s.trim()));
154
+ }
155
+ features.imports.push(match[2]);
156
+ }
157
+
158
+ // Extract variable names (simple heuristic)
159
+ const varMatches = code.matchAll(/(?:const|let|var)\s+(\w+)/g);
160
+ for (const match of varMatches) {
161
+ if (!features.functions.includes(match[1])) {
162
+ features.variables.push(match[1]);
163
+ }
164
+ }
165
+
166
+ return features;
167
+ }
168
+
169
+ /**
170
+ * Lấy keywords quan trọng
171
+ */
172
+ getKeywords(tokens, language) {
173
+ const codeKeywords = {
174
+ javascript: ['function', 'class', 'const', 'let', 'var', 'async', 'await', 'return', 'if', 'for', 'while', 'import', 'export'],
175
+ typescript: ['interface', 'type', 'enum', 'namespace', 'extends', 'implements', 'function', 'class', 'const', 'let', 'var', 'async', 'await'],
176
+ python: ['def', 'class', 'import', 'from', 'return', 'if', 'for', 'while'],
177
+ };
178
+
179
+ const keywords = codeKeywords[language] || codeKeywords.javascript;
180
+ return tokens.filter(token => keywords.includes(token));
181
+ }
182
+
183
+ /**
184
+ * Hash string thành số
185
+ */
186
+ hashString(str) {
187
+ let hash = 0;
188
+ for (let i = 0; i < str.length; i++) {
189
+ const char = str.charCodeAt(i);
190
+ hash = ((hash << 5) - hash) + char;
191
+ hash = hash & hash;
192
+ }
193
+ return Math.abs(hash);
194
+ }
195
+
196
+ /**
197
+ * Lấy index cho token trong vocabulary
198
+ */
199
+ getTokenIndex(token) {
200
+ if (!this.vocabulary.has(token)) {
201
+ this.vocabulary.set(token, this.vocabulary.size);
202
+ }
203
+ return this.vocabulary.get(token);
204
+ }
205
+
206
+ /**
207
+ * Normalize vector (L2 normalization)
208
+ */
209
+ normalize(vector) {
210
+ const magnitude = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0));
211
+ if (magnitude === 0) return vector;
212
+ return vector.map(val => val / magnitude);
213
+ }
214
+
215
+ /**
216
+ * Tính cosine similarity giữa 2 embeddings
217
+ */
218
+ cosineSimilarity(embedding1, embedding2) {
219
+ let dotProduct = 0;
220
+ for (let i = 0; i < embedding1.length; i++) {
221
+ dotProduct += embedding1[i] * embedding2[i];
222
+ }
223
+ return dotProduct; // Vectors đã normalize nên chỉ cần dot product
224
+ }
225
+
226
+ /**
227
+ * Batch embeddings
228
+ */
229
+ embedBatch(texts) {
230
+ return texts.map(text => this.embedCode(text));
231
+ }
232
+ }
233
+
234
+ export default SimpleEmbedder;
@@ -0,0 +1,344 @@
1
+ /**
2
+ * Smart Router - Intelligent Local vs Mercury Decision Engine
3
+ *
4
+ * Tự động quyết định khi nào dùng local (90%) vs Mercury (95%)
5
+ * dựa trên: query complexity, cost optimization, và user preferences
6
+ */
7
+
8
+ export class SmartRouter {
9
+ constructor(options = {}) {
10
+ this.dashboardClient = options.dashboardClient || null;
11
+ this.localAccuracy = 0.90;
12
+ this.mercuryAccuracy = 0.95;
13
+ this.costPerMercuryCall = 0.01; // $0.01 per call
14
+
15
+ // User configuration từ Dashboard
16
+ this.config = {
17
+ enableMercury: false,
18
+ smartRoutingMode: 'auto', // 'always_local', 'auto', 'always_mercury'
19
+ rerankThreshold: 0.7,
20
+ monthlyBudget: 20,
21
+ currentUsage: 0
22
+ };
23
+
24
+ // Query analysis patterns
25
+ this.complexityPatterns = {
26
+ high: [
27
+ /\b(find|search|locate)\b.*\b(all|every|each)\b/i,
28
+ /\b(compare|difference|versus|vs)\b/i,
29
+ /\b(relationship|connection|link|reference)\b/i,
30
+ /\b(impact|affect|change|modify)\b.*\b(multiple|many|several)\b/i,
31
+ /\b(best|optimal|recommend|suggest)\b/i,
32
+ /\b(why|how|explain|understand)\b/i,
33
+ /\b(similar|related|like)\b/i,
34
+ /\b(dependencies|imports|uses)\b/i,
35
+ /\b(refactor|restructure|reorganize)\b/i,
36
+ /\b(natural language|plain text|sentence)\b/i
37
+ ],
38
+ medium: [
39
+ /\b(update|change|modify|edit)\b/i,
40
+ /\b(fix|correct|repair|solve)\b/i,
41
+ /\b(add|create|new|insert)\b/i,
42
+ /\b(remove|delete|clear)\b/i,
43
+ /\b(validate|check|verify|test)\b/i,
44
+ /\b(handle|process|manage)\b/i
45
+ ]
46
+ };
47
+
48
+ // Historical performance tracking
49
+ this.history = {
50
+ localSuccess: 0,
51
+ localTotal: 0,
52
+ mercurySuccess: 0,
53
+ mercuryTotal: 0,
54
+ queryTypes: new Map()
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Load configuration từ Dashboard
60
+ */
61
+ async loadConfig() {
62
+ if (!this.dashboardClient) return;
63
+
64
+ try {
65
+ const config = await this.dashboardClient.getSearchConfig();
66
+ this.config = {
67
+ enableMercury: config.enable_mercury_rerank ?? false,
68
+ smartRoutingMode: config.smart_routing_mode ?? 'auto',
69
+ rerankThreshold: config.rerank_threshold ?? 0.7,
70
+ monthlyBudget: config.monthly_budget ?? 20,
71
+ currentUsage: config.usage_this_month ?? 0
72
+ };
73
+ } catch (error) {
74
+ console.warn('[SmartRouter] Failed to load config:', error.message);
75
+ // Fallback: use default config
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Main decision method - chọn local hay Mercury
81
+ */
82
+ async shouldUseMercury(query, localResults = null) {
83
+ // 1. Kiểm tra cơ bản
84
+ if (!this.config.enableMercury) {
85
+ return { useMercury: false, reason: 'Mercury disabled in settings' };
86
+ }
87
+
88
+ // 2. Check mode
89
+ if (this.config.smartRoutingMode === 'always_local') {
90
+ return { useMercury: false, reason: 'Mode: always_local' };
91
+ }
92
+
93
+ if (this.config.smartRoutingMode === 'always_mercury') {
94
+ const withinBudget = await this.checkBudget();
95
+ if (withinBudget) {
96
+ return { useMercury: true, reason: 'Mode: always_mercury' };
97
+ }
98
+ return { useMercury: false, reason: 'Budget exceeded, fallback to local' };
99
+ }
100
+
101
+ // 3. Auto mode - smart decision
102
+ return this.makeSmartDecision(query, localResults);
103
+ }
104
+
105
+ /**
106
+ * Smart decision logic
107
+ */
108
+ async makeSmartDecision(query, localResults) {
109
+ const scores = {
110
+ queryComplexity: this.analyzeQueryComplexity(query),
111
+ confidenceGap: this.calculateConfidenceGap(localResults),
112
+ budgetStatus: await this.checkBudget() ? 1 : 0,
113
+ historicalSuccess: this.getHistoricalSuccess(query),
114
+ urgency: this.analyzeUrgency(query)
115
+ };
116
+
117
+ // Weighted scoring
118
+ const totalScore =
119
+ (scores.queryComplexity * 0.30) +
120
+ (scores.confidenceGap * 0.25) +
121
+ (scores.budgetStatus * 0.20) +
122
+ (scores.historicalSuccess * 0.15) +
123
+ (scores.urgency * 0.10);
124
+
125
+ // Decision
126
+ const useMercury = totalScore >= this.config.rerankThreshold;
127
+
128
+ return {
129
+ useMercury,
130
+ reason: useMercury ?
131
+ `Complex query detected (score: ${totalScore.toFixed(2)})` :
132
+ `Simple query, local sufficient (score: ${totalScore.toFixed(2)})`,
133
+ scores,
134
+ confidence: totalScore
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Analyze query complexity (0-1)
140
+ */
141
+ analyzeQueryComplexity(query) {
142
+ const queryLower = query.toLowerCase();
143
+ let complexity = 0.5; // Base
144
+
145
+ // Check high complexity patterns
146
+ for (const pattern of this.complexityPatterns.high) {
147
+ if (pattern.test(queryLower)) {
148
+ complexity += 0.15;
149
+ }
150
+ }
151
+
152
+ // Check medium complexity patterns
153
+ for (const pattern of this.complexityPatterns.medium) {
154
+ if (pattern.test(queryLower)) {
155
+ complexity += 0.05;
156
+ }
157
+ }
158
+
159
+ // Query length factor
160
+ const wordCount = query.split(/\s+/).length;
161
+ if (wordCount > 10) complexity += 0.1;
162
+ if (wordCount > 20) complexity += 0.1;
163
+
164
+ // Multiple operations
165
+ const operations = ['create', 'update', 'delete', 'find', 'validate', 'handle'];
166
+ const opCount = operations.filter(op => queryLower.includes(op)).length;
167
+ complexity += opCount * 0.05;
168
+
169
+ return Math.min(1, complexity);
170
+ }
171
+
172
+ /**
173
+ * Calculate confidence gap from local results
174
+ */
175
+ calculateConfidenceGap(localResults) {
176
+ if (!localResults || localResults.length === 0) {
177
+ return 1.0; // No results = need Mercury
178
+ }
179
+
180
+ // Check top result confidence
181
+ const topScore = localResults[0].score || localResults[0].similarity || 0;
182
+
183
+ // Check score distribution
184
+ if (localResults.length >= 2) {
185
+ const secondScore = localResults[1].score || localResults[1].similarity || 0;
186
+ const gap = topScore - secondScore;
187
+
188
+ // If gap is small, need Mercury to differentiate
189
+ if (gap < 0.1) return 0.8;
190
+ if (gap < 0.2) return 0.5;
191
+ }
192
+
193
+ // Low confidence in top result
194
+ if (topScore < 0.6) return 0.7;
195
+ if (topScore < 0.8) return 0.4;
196
+
197
+ return 0.2; // High confidence, no need for Mercury
198
+ }
199
+
200
+ /**
201
+ * Check if within budget
202
+ */
203
+ async checkBudget() {
204
+ if (this.config.monthlyBudget === 0) return true; // Unlimited
205
+
206
+ const projectedCost = (this.config.currentUsage + 1) * this.costPerMercuryCall;
207
+ const remaining = this.config.monthlyBudget - projectedCost;
208
+
209
+ // Use Mercury if we have 20% budget remaining
210
+ return remaining > (this.config.monthlyBudget * 0.2);
211
+ }
212
+
213
+ /**
214
+ * Get historical success rate for similar queries
215
+ */
216
+ getHistoricalSuccess(query) {
217
+ // Extract query type
218
+ const queryType = this.extractQueryType(query);
219
+ const history = this.history.queryTypes.get(queryType);
220
+
221
+ if (!history || history.total < 5) {
222
+ return 0.5; // Neutral if not enough data
223
+ }
224
+
225
+ const successRate = history.success / history.total;
226
+
227
+ // If local has high success rate, prefer local
228
+ if (successRate > 0.8) return 0.2;
229
+ if (successRate > 0.6) return 0.4;
230
+
231
+ // Low success rate, prefer Mercury
232
+ return 0.8;
233
+ }
234
+
235
+ /**
236
+ * Analyze urgency (0-1)
237
+ */
238
+ analyzeUrgency(query) {
239
+ const queryLower = query.toLowerCase();
240
+
241
+ // Urgent keywords
242
+ const urgentWords = ['fix', 'bug', 'error', 'crash', 'urgent', 'critical', 'asap'];
243
+ if (urgentWords.some(w => queryLower.includes(w))) {
244
+ return 0.9; // Use best accuracy (Mercury)
245
+ }
246
+
247
+ // Exploration keywords
248
+ const exploreWords = ['explore', 'understand', 'learn', 'research', 'investigate'];
249
+ if (exploreWords.some(w => queryLower.includes(w))) {
250
+ return 0.3; // Can use local, not urgent
251
+ }
252
+
253
+ return 0.5;
254
+ }
255
+
256
+ /**
257
+ * Extract query type for history tracking
258
+ */
259
+ extractQueryType(query) {
260
+ const queryLower = query.toLowerCase();
261
+
262
+ if (/\b(find|search|locate|get)\b/.test(queryLower)) return 'search';
263
+ if (/\b(update|change|modify|edit)\b/.test(queryLower)) return 'update';
264
+ if (/\b(create|add|new|insert)\b/.test(queryLower)) return 'create';
265
+ if (/\b(delete|remove|clear)\b/.test(queryLower)) return 'delete';
266
+ if (/\b(fix|correct|repair)\b/.test(queryLower)) return 'fix';
267
+ if (/\b(validate|check|verify|test)\b/.test(queryLower)) return 'validate';
268
+ if (/\b(refactor|restructure)\b/.test(queryLower)) return 'refactor';
269
+
270
+ return 'general';
271
+ }
272
+
273
+ /**
274
+ * Record result for learning
275
+ */
276
+ recordResult(query, usedMercury, success) {
277
+ const queryType = this.extractQueryType(query);
278
+
279
+ // Update query type history
280
+ if (!this.history.queryTypes.has(queryType)) {
281
+ this.history.queryTypes.set(queryType, { success: 0, total: 0 });
282
+ }
283
+
284
+ const history = this.history.queryTypes.get(queryType);
285
+ history.total++;
286
+ if (success) history.success++;
287
+
288
+ // Update global stats
289
+ if (usedMercury) {
290
+ this.history.mercuryTotal++;
291
+ if (success) this.history.mercurySuccess++;
292
+ } else {
293
+ this.history.localTotal++;
294
+ if (success) this.history.localSuccess++;
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Log usage to Dashboard
300
+ */
301
+ async logUsage(query, usedMercury, duration) {
302
+ if (!this.dashboardClient) return;
303
+
304
+ try {
305
+ await this.dashboardClient.logSearchUsage({
306
+ query,
307
+ usedMercury,
308
+ duration,
309
+ timestamp: Date.now()
310
+ });
311
+ } catch (error) {
312
+ console.warn('[SmartRouter] Failed to log usage:', error.message);
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Get stats
318
+ */
319
+ getStats() {
320
+ const localRate = this.history.localTotal > 0 ?
321
+ this.history.localSuccess / this.history.localTotal : 0;
322
+ const mercuryRate = this.history.mercuryTotal > 0 ?
323
+ this.history.mercurySuccess / this.history.mercuryTotal : 0;
324
+
325
+ return {
326
+ config: this.config,
327
+ history: {
328
+ local: {
329
+ total: this.history.localTotal,
330
+ success: this.history.localSuccess,
331
+ rate: localRate
332
+ },
333
+ mercury: {
334
+ total: this.history.mercuryTotal,
335
+ success: this.history.mercurySuccess,
336
+ rate: mercuryRate
337
+ }
338
+ },
339
+ estimatedMonthlyCost: this.history.mercuryTotal * this.costPerMercuryCall
340
+ };
341
+ }
342
+ }
343
+
344
+ export default SmartRouter;