@gotza02/sequential-thinking 10000.0.8 → 10000.1.0
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.
- package/README.md +249 -39
- package/dist/dashboard/server.d.ts +79 -2
- package/dist/dashboard/server.js +466 -61
- package/dist/index.js +1 -4
- package/dist/tools/sports/core/alert-manager.d.ts +145 -0
- package/dist/tools/sports/core/alert-manager.js +380 -0
- package/dist/tools/sports/core/cache.d.ts +19 -8
- package/dist/tools/sports/core/cache.js +95 -38
- package/dist/tools/sports/core/circuit-breaker.d.ts +40 -0
- package/dist/tools/sports/core/circuit-breaker.js +99 -0
- package/dist/tools/sports/core/constants.d.ts +63 -4
- package/dist/tools/sports/core/constants.js +86 -11
- package/dist/tools/sports/core/data-quality.d.ts +80 -0
- package/dist/tools/sports/core/data-quality.js +460 -0
- package/dist/tools/sports/core/historical-analyzer.d.ts +108 -0
- package/dist/tools/sports/core/historical-analyzer.js +461 -0
- package/dist/tools/sports/core/index.d.ts +13 -0
- package/dist/tools/sports/core/index.js +16 -0
- package/dist/tools/sports/core/ml-prediction.d.ts +134 -0
- package/dist/tools/sports/core/ml-prediction.js +402 -0
- package/dist/tools/sports/core/realtime-manager.d.ts +102 -0
- package/dist/tools/sports/core/realtime-manager.js +331 -0
- package/dist/tools/sports/core/retry.d.ts +29 -0
- package/dist/tools/sports/core/retry.js +77 -0
- package/dist/tools/sports/core/types.d.ts +40 -1
- package/package.json +1 -1
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* TTL-based caching with
|
|
2
|
+
* ENHANCED CACHE SERVICE
|
|
3
|
+
* TTL-based caching with stale-while-revalidate pattern
|
|
4
4
|
*/
|
|
5
5
|
import * as fs from 'fs/promises';
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import { CACHE_CONFIG } from './constants.js';
|
|
8
8
|
import { logger, createSafeRegex } from '../../../utils.js';
|
|
9
9
|
/**
|
|
10
|
-
* CacheService - Thread-safe TTL-based caching with
|
|
10
|
+
* CacheService - Thread-safe TTL-based caching with stale-while-revalidate
|
|
11
11
|
*/
|
|
12
12
|
export class CacheService {
|
|
13
13
|
cache = new Map();
|
|
14
14
|
cachePath;
|
|
15
15
|
maxAge;
|
|
16
|
+
staleAge;
|
|
16
17
|
maxSize;
|
|
17
18
|
savePending = false;
|
|
18
19
|
saveTimer = null;
|
|
19
|
-
constructor(cachePath = CACHE_CONFIG.CACHE_PATH, maxAge = CACHE_CONFIG.DEFAULT_TTL, maxSize = CACHE_CONFIG.MAX_SIZE) {
|
|
20
|
+
constructor(cachePath = CACHE_CONFIG.CACHE_PATH, maxAge = CACHE_CONFIG.DEFAULT_TTL, maxSize = CACHE_CONFIG.MAX_SIZE, staleAge) {
|
|
20
21
|
this.cachePath = path.resolve(cachePath);
|
|
21
22
|
this.maxAge = maxAge;
|
|
23
|
+
this.staleAge = staleAge || maxAge * 2;
|
|
22
24
|
this.maxSize = maxSize;
|
|
23
25
|
this.loadFromFile();
|
|
24
26
|
}
|
|
@@ -36,11 +38,12 @@ export class CacheService {
|
|
|
36
38
|
if (!entry) {
|
|
37
39
|
return null;
|
|
38
40
|
}
|
|
39
|
-
// Check if expired
|
|
41
|
+
// Check if expired (including stale period)
|
|
40
42
|
const now = Date.now();
|
|
41
|
-
|
|
43
|
+
const totalTtl = entry.ttl + entry.staleTtl;
|
|
44
|
+
if (now - entry.timestamp > totalTtl) {
|
|
42
45
|
this.cache.delete(key);
|
|
43
|
-
logger.debug(`Cache expired: ${key}`);
|
|
46
|
+
logger.debug(`Cache expired (including stale): ${key}`);
|
|
44
47
|
return null;
|
|
45
48
|
}
|
|
46
49
|
// Increment hit counter
|
|
@@ -51,7 +54,7 @@ export class CacheService {
|
|
|
51
54
|
/**
|
|
52
55
|
* Set a value in cache
|
|
53
56
|
*/
|
|
54
|
-
set(key, data, ttl) {
|
|
57
|
+
set(key, data, ttl, staleTtl) {
|
|
55
58
|
const now = Date.now();
|
|
56
59
|
// Check if we need to evict old entries
|
|
57
60
|
if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
|
|
@@ -62,11 +65,64 @@ export class CacheService {
|
|
|
62
65
|
data,
|
|
63
66
|
timestamp: now,
|
|
64
67
|
ttl: ttl || this.maxAge,
|
|
68
|
+
staleTtl: staleTtl || this.staleAge,
|
|
65
69
|
hits: 0,
|
|
66
70
|
});
|
|
67
|
-
logger.debug(`Cache set: ${key} (TTL: ${ttl || this.maxAge}ms)`);
|
|
71
|
+
logger.debug(`Cache set: ${key} (TTL: ${ttl || this.maxAge}ms, Stale: ${staleTtl || this.staleAge}ms)`);
|
|
68
72
|
this.scheduleSave();
|
|
69
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Get or set with stale-while-revalidate pattern
|
|
76
|
+
* Returns cached value immediately, refreshes in background if stale
|
|
77
|
+
*/
|
|
78
|
+
async getOrSetWithStaleRevalidate(key, factory, ttl, staleTtl) {
|
|
79
|
+
const entry = this.cache.get(key);
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
const effectiveTtl = ttl || this.maxAge;
|
|
82
|
+
const effectiveStaleTtl = staleTtl || this.staleAge;
|
|
83
|
+
if (entry) {
|
|
84
|
+
const age = now - entry.timestamp;
|
|
85
|
+
// Data is fresh - use it
|
|
86
|
+
if (age < effectiveTtl) {
|
|
87
|
+
entry.hits++;
|
|
88
|
+
return entry.data;
|
|
89
|
+
}
|
|
90
|
+
// Data is stale but within stale TTL - use it and refresh in background
|
|
91
|
+
if (age < effectiveTtl + effectiveStaleTtl) {
|
|
92
|
+
entry.hits++;
|
|
93
|
+
if (!entry.isRefreshing) {
|
|
94
|
+
this.refreshInBackground(key, factory, effectiveTtl, effectiveStaleTtl);
|
|
95
|
+
}
|
|
96
|
+
return entry.data;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// No data or expired beyond stale TTL - fetch new data
|
|
100
|
+
const value = await factory();
|
|
101
|
+
this.set(key, value, effectiveTtl, effectiveStaleTtl);
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Refresh data in background
|
|
106
|
+
*/
|
|
107
|
+
async refreshInBackground(key, factory, ttl, staleTtl) {
|
|
108
|
+
const entry = this.cache.get(key);
|
|
109
|
+
if (entry) {
|
|
110
|
+
entry.isRefreshing = true;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const value = await factory();
|
|
114
|
+
this.set(key, value, ttl, staleTtl);
|
|
115
|
+
logger.debug(`Background refresh successful: ${key}`);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
logger.error(`Background refresh failed for ${key}: ${error}`);
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
if (entry) {
|
|
122
|
+
entry.isRefreshing = false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
70
126
|
/**
|
|
71
127
|
* Check if a key exists and is not expired
|
|
72
128
|
*/
|
|
@@ -75,7 +131,8 @@ export class CacheService {
|
|
|
75
131
|
if (!entry)
|
|
76
132
|
return false;
|
|
77
133
|
const now = Date.now();
|
|
78
|
-
|
|
134
|
+
const totalTtl = entry.ttl + entry.staleTtl;
|
|
135
|
+
if (now - entry.timestamp > totalTtl) {
|
|
79
136
|
this.cache.delete(key);
|
|
80
137
|
return false;
|
|
81
138
|
}
|
|
@@ -100,12 +157,16 @@ export class CacheService {
|
|
|
100
157
|
*/
|
|
101
158
|
getStats() {
|
|
102
159
|
const now = Date.now();
|
|
103
|
-
const entries = Array.from(this.cache.values()).map(entry =>
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
160
|
+
const entries = Array.from(this.cache.values()).map(entry => {
|
|
161
|
+
const age = now - entry.timestamp;
|
|
162
|
+
return {
|
|
163
|
+
key: entry.key,
|
|
164
|
+
hits: entry.hits,
|
|
165
|
+
age,
|
|
166
|
+
ttl: entry.ttl,
|
|
167
|
+
isStale: age > entry.ttl,
|
|
168
|
+
};
|
|
169
|
+
});
|
|
109
170
|
return {
|
|
110
171
|
size: this.cache.size,
|
|
111
172
|
maxSize: this.maxSize,
|
|
@@ -120,7 +181,8 @@ export class CacheService {
|
|
|
120
181
|
const now = Date.now();
|
|
121
182
|
let removed = 0;
|
|
122
183
|
for (const [key, entry] of this.cache.entries()) {
|
|
123
|
-
|
|
184
|
+
const totalTtl = entry.ttl + entry.staleTtl;
|
|
185
|
+
if (now - entry.timestamp > totalTtl) {
|
|
124
186
|
this.cache.delete(key);
|
|
125
187
|
removed++;
|
|
126
188
|
}
|
|
@@ -132,20 +194,22 @@ export class CacheService {
|
|
|
132
194
|
return removed;
|
|
133
195
|
}
|
|
134
196
|
/**
|
|
135
|
-
* Evict the oldest entry (LRU)
|
|
197
|
+
* Evict the oldest entry (LRU with hit weighting)
|
|
136
198
|
*/
|
|
137
199
|
evictOldest() {
|
|
138
200
|
let oldestKey = null;
|
|
139
|
-
let
|
|
201
|
+
let lowestScore = Infinity;
|
|
202
|
+
// Evict based on age weighted by hits
|
|
140
203
|
for (const [key, entry] of this.cache.entries()) {
|
|
141
|
-
|
|
142
|
-
|
|
204
|
+
const score = (Date.now() - entry.timestamp) / (entry.hits + 1);
|
|
205
|
+
if (score < lowestScore) {
|
|
206
|
+
lowestScore = score;
|
|
143
207
|
oldestKey = key;
|
|
144
208
|
}
|
|
145
209
|
}
|
|
146
210
|
if (oldestKey) {
|
|
147
211
|
this.cache.delete(oldestKey);
|
|
148
|
-
logger.debug(`Evicted
|
|
212
|
+
logger.debug(`Evicted LRU entry: ${oldestKey}`);
|
|
149
213
|
}
|
|
150
214
|
}
|
|
151
215
|
/**
|
|
@@ -158,7 +222,7 @@ export class CacheService {
|
|
|
158
222
|
this.saveTimer = setTimeout(() => {
|
|
159
223
|
this.saveToFile();
|
|
160
224
|
this.saveTimer = null;
|
|
161
|
-
}, 1000);
|
|
225
|
+
}, 1000);
|
|
162
226
|
}
|
|
163
227
|
/**
|
|
164
228
|
* Save cache to file
|
|
@@ -192,7 +256,8 @@ export class CacheService {
|
|
|
192
256
|
const now = Date.now();
|
|
193
257
|
let restored = 0;
|
|
194
258
|
for (const [key, entry] of entries) {
|
|
195
|
-
|
|
259
|
+
const totalTtl = entry.ttl + entry.staleTtl;
|
|
260
|
+
if (now - entry.timestamp < totalTtl) {
|
|
196
261
|
this.cache.set(key, entry);
|
|
197
262
|
restored++;
|
|
198
263
|
}
|
|
@@ -200,7 +265,6 @@ export class CacheService {
|
|
|
200
265
|
logger.info(`Cache loaded: ${restored}/${entries.length} entries restored`);
|
|
201
266
|
}
|
|
202
267
|
catch (error) {
|
|
203
|
-
// File doesn't exist or is invalid - start fresh
|
|
204
268
|
logger.debug('No existing cache file found, starting fresh');
|
|
205
269
|
}
|
|
206
270
|
}
|
|
@@ -217,14 +281,8 @@ export class CacheService {
|
|
|
217
281
|
/**
|
|
218
282
|
* Get or set pattern - returns cached value or computes and caches it
|
|
219
283
|
*/
|
|
220
|
-
async getOrSet(key, factory, ttl) {
|
|
221
|
-
|
|
222
|
-
if (cached !== null) {
|
|
223
|
-
return cached;
|
|
224
|
-
}
|
|
225
|
-
const value = await factory();
|
|
226
|
-
this.set(key, value, ttl);
|
|
227
|
-
return value;
|
|
284
|
+
async getOrSet(key, factory, ttl, staleTtl) {
|
|
285
|
+
return this.getOrSetWithStaleRevalidate(key, factory, ttl, staleTtl);
|
|
228
286
|
}
|
|
229
287
|
/**
|
|
230
288
|
* Get multiple keys at once
|
|
@@ -242,16 +300,15 @@ export class CacheService {
|
|
|
242
300
|
/**
|
|
243
301
|
* Set multiple keys at once
|
|
244
302
|
*/
|
|
245
|
-
setMultiple(entries, ttl) {
|
|
303
|
+
setMultiple(entries, ttl, staleTtl) {
|
|
246
304
|
for (const [key, value] of entries.entries()) {
|
|
247
|
-
this.set(key, value, ttl);
|
|
305
|
+
this.set(key, value, ttl, staleTtl);
|
|
248
306
|
}
|
|
249
307
|
}
|
|
250
308
|
/**
|
|
251
309
|
* Invalidate cache by pattern
|
|
252
310
|
*/
|
|
253
311
|
invalidatePattern(pattern) {
|
|
254
|
-
// Use safe regex creation to prevent ReDoS attacks
|
|
255
312
|
const regex = createSafeRegex(pattern);
|
|
256
313
|
let removed = 0;
|
|
257
314
|
for (const key of this.cache.keys()) {
|
|
@@ -288,10 +345,10 @@ let globalCache = null;
|
|
|
288
345
|
export function getGlobalCache() {
|
|
289
346
|
if (!globalCache) {
|
|
290
347
|
globalCache = new CacheService();
|
|
291
|
-
// Cleanup expired entries every
|
|
348
|
+
// Cleanup expired entries every 2 minutes
|
|
292
349
|
setInterval(() => {
|
|
293
350
|
globalCache?.cleanup();
|
|
294
|
-
},
|
|
351
|
+
}, 2 * 60 * 1000);
|
|
295
352
|
}
|
|
296
353
|
return globalCache;
|
|
297
354
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CIRCUIT BREAKER PATTERN
|
|
3
|
+
* Prevents cascading failures by stopping requests to failing services
|
|
4
|
+
*/
|
|
5
|
+
export declare class CircuitBreaker {
|
|
6
|
+
private readonly name;
|
|
7
|
+
private readonly failureThreshold;
|
|
8
|
+
private readonly resetTimeout;
|
|
9
|
+
private readonly halfOpenMaxCalls;
|
|
10
|
+
private failures;
|
|
11
|
+
private lastFailureTime;
|
|
12
|
+
private state;
|
|
13
|
+
private successCount;
|
|
14
|
+
constructor(name: string, failureThreshold?: number, resetTimeout?: number, halfOpenMaxCalls?: number);
|
|
15
|
+
/**
|
|
16
|
+
* Execute a function with circuit breaker protection
|
|
17
|
+
*/
|
|
18
|
+
execute<T>(fn: () => Promise<T>, context?: string): Promise<T>;
|
|
19
|
+
private onSuccess;
|
|
20
|
+
private onFailure;
|
|
21
|
+
getStatus(): {
|
|
22
|
+
name: string;
|
|
23
|
+
state: "closed" | "open" | "half-open";
|
|
24
|
+
failures: number;
|
|
25
|
+
lastFailure: number | null;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export declare class CircuitBreakerOpenError extends Error {
|
|
29
|
+
readonly retryAfter: number;
|
|
30
|
+
constructor(message: string, retryAfter: number);
|
|
31
|
+
}
|
|
32
|
+
export declare function getCircuitBreaker(name: string, config?: {
|
|
33
|
+
failureThreshold?: number;
|
|
34
|
+
resetTimeout?: number;
|
|
35
|
+
}): CircuitBreaker;
|
|
36
|
+
export declare function getAllCircuitBreakerStatus(): Array<{
|
|
37
|
+
name: string;
|
|
38
|
+
state: string;
|
|
39
|
+
failures: number;
|
|
40
|
+
}>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CIRCUIT BREAKER PATTERN
|
|
3
|
+
* Prevents cascading failures by stopping requests to failing services
|
|
4
|
+
*/
|
|
5
|
+
import { logger } from '../../../utils.js';
|
|
6
|
+
export class CircuitBreaker {
|
|
7
|
+
name;
|
|
8
|
+
failureThreshold;
|
|
9
|
+
resetTimeout;
|
|
10
|
+
halfOpenMaxCalls;
|
|
11
|
+
failures = 0;
|
|
12
|
+
lastFailureTime = null;
|
|
13
|
+
state = 'closed';
|
|
14
|
+
successCount = 0;
|
|
15
|
+
constructor(name, failureThreshold = 5, resetTimeout = 60000, halfOpenMaxCalls = 3) {
|
|
16
|
+
this.name = name;
|
|
17
|
+
this.failureThreshold = failureThreshold;
|
|
18
|
+
this.resetTimeout = resetTimeout;
|
|
19
|
+
this.halfOpenMaxCalls = halfOpenMaxCalls;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Execute a function with circuit breaker protection
|
|
23
|
+
*/
|
|
24
|
+
async execute(fn, context) {
|
|
25
|
+
if (this.state === 'open') {
|
|
26
|
+
const timeSinceLastFailure = Date.now() - (this.lastFailureTime || 0);
|
|
27
|
+
if (timeSinceLastFailure > this.resetTimeout) {
|
|
28
|
+
logger.info(`[CircuitBreaker:${this.name}] Transitioning to HALF-OPEN`);
|
|
29
|
+
this.state = 'half-open';
|
|
30
|
+
this.successCount = 0;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
const remainingTime = Math.ceil((this.resetTimeout - timeSinceLastFailure) / 1000);
|
|
34
|
+
throw new CircuitBreakerOpenError(`Circuit breaker '${this.name}' is OPEN. Retry in ${remainingTime}s`, remainingTime);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const result = await fn();
|
|
39
|
+
this.onSuccess();
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
this.onFailure(error instanceof Error ? error : new Error(String(error)));
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
onSuccess() {
|
|
48
|
+
if (this.state === 'half-open') {
|
|
49
|
+
this.successCount++;
|
|
50
|
+
if (this.successCount >= this.halfOpenMaxCalls) {
|
|
51
|
+
logger.info(`[CircuitBreaker:${this.name}] Transitioning to CLOSED`);
|
|
52
|
+
this.state = 'closed';
|
|
53
|
+
this.failures = 0;
|
|
54
|
+
this.successCount = 0;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
this.failures = Math.max(0, this.failures - 1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
onFailure(error) {
|
|
62
|
+
this.failures++;
|
|
63
|
+
this.lastFailureTime = Date.now();
|
|
64
|
+
logger.warn(`[CircuitBreaker:${this.name}] Failure ${this.failures}/${this.failureThreshold}: ${error.message}`);
|
|
65
|
+
if (this.failures >= this.failureThreshold) {
|
|
66
|
+
logger.error(`[CircuitBreaker:${this.name}] Threshold reached. Transitioning to OPEN`);
|
|
67
|
+
this.state = 'open';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
getStatus() {
|
|
71
|
+
return {
|
|
72
|
+
name: this.name,
|
|
73
|
+
state: this.state,
|
|
74
|
+
failures: this.failures,
|
|
75
|
+
lastFailure: this.lastFailureTime,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export class CircuitBreakerOpenError extends Error {
|
|
80
|
+
retryAfter;
|
|
81
|
+
constructor(message, retryAfter) {
|
|
82
|
+
super(message);
|
|
83
|
+
this.retryAfter = retryAfter;
|
|
84
|
+
this.name = 'CircuitBreakerOpenError';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Global circuit breakers registry
|
|
88
|
+
const circuitBreakers = new Map();
|
|
89
|
+
export function getCircuitBreaker(name, config) {
|
|
90
|
+
if (!circuitBreakers.has(name)) {
|
|
91
|
+
circuitBreakers.set(name, new CircuitBreaker(name, config?.failureThreshold, config?.resetTimeout));
|
|
92
|
+
}
|
|
93
|
+
return circuitBreakers.get(name);
|
|
94
|
+
}
|
|
95
|
+
export function getAllCircuitBreakerStatus() {
|
|
96
|
+
return Array.from(circuitBreakers.values()).map(cb => ({
|
|
97
|
+
...cb.getStatus()
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
@@ -10,25 +10,40 @@ export declare const API_CONFIG: {
|
|
|
10
10
|
readonly FOOTBALL_DATA_BASE: "https://api.football-data.org/v4";
|
|
11
11
|
readonly SPORTS_DB_KEY: string;
|
|
12
12
|
readonly SPORTS_DB_BASE: "https://www.thesportsdb.com/api/v1/json";
|
|
13
|
+
readonly ODDS_API_KEY: string;
|
|
14
|
+
readonly ODDS_API_BASE: "https://api.the-odds-api.com/v4";
|
|
15
|
+
readonly SPORTRADAR_KEY: string;
|
|
16
|
+
readonly SPORTRADAR_BASE: "https://api.sportradar.us/soccer/trial/v4";
|
|
13
17
|
readonly RATE_LIMITS: {
|
|
14
18
|
readonly API_FOOTBALL: 10;
|
|
15
19
|
readonly FOOTBALL_DATA: 10;
|
|
16
20
|
readonly SPORTS_DB: 30;
|
|
21
|
+
readonly ODDS_API: 20;
|
|
22
|
+
readonly SPORTRADAR: 100;
|
|
17
23
|
};
|
|
18
24
|
};
|
|
19
25
|
export declare const CACHE_CONFIG: {
|
|
20
26
|
readonly DEFAULT_TTL: number;
|
|
21
27
|
readonly TTL: {
|
|
22
28
|
readonly LIVE_SCORES: number;
|
|
29
|
+
readonly LIVE_EVENTS: number;
|
|
30
|
+
readonly ODDS: number;
|
|
23
31
|
readonly MATCH_DETAILS: number;
|
|
24
32
|
readonly STANDINGS: number;
|
|
25
33
|
readonly TEAM_STATS: number;
|
|
26
34
|
readonly PLAYER_STATS: number;
|
|
27
|
-
readonly ODDS: number;
|
|
28
35
|
readonly TRANSFER_NEWS: number;
|
|
36
|
+
readonly PREDICTIONS: number;
|
|
37
|
+
};
|
|
38
|
+
readonly STALE_MULTIPLIERS: {
|
|
39
|
+
readonly LIVE_SCORES: 2;
|
|
40
|
+
readonly ODDS: 2;
|
|
41
|
+
readonly MATCH_DETAILS: 2;
|
|
42
|
+
readonly STANDINGS: 1.5;
|
|
43
|
+
readonly TEAM_STATS: 1.5;
|
|
29
44
|
};
|
|
30
45
|
readonly CACHE_PATH: string;
|
|
31
|
-
readonly MAX_SIZE:
|
|
46
|
+
readonly MAX_SIZE: 2000;
|
|
32
47
|
};
|
|
33
48
|
export declare const SCRAPER_CONFIG: {
|
|
34
49
|
readonly PRIORITY_DOMAINS: readonly ["understat.com", "bbc.co.uk/sport", "sportsmole.co.uk", "skysports.com", "goal.com", "thesquareball.net", "fbref.com", "whoscored.com", "sofascore.com", "flashscore.com"];
|
|
@@ -172,9 +187,53 @@ export declare const ERRORS: {
|
|
|
172
187
|
readonly TIMEOUT: "Request timed out.";
|
|
173
188
|
readonly INVALID_RESPONSE: "Invalid response from API.";
|
|
174
189
|
readonly ALL_PROVIDERS_FAILED: "All data providers failed.";
|
|
190
|
+
readonly CIRCUIT_BREAKER_OPEN: "Service temporarily unavailable due to repeated failures.";
|
|
191
|
+
};
|
|
192
|
+
export declare const ALERT_CONFIG: {
|
|
193
|
+
readonly CHECK_INTERVAL: 10000;
|
|
194
|
+
readonly DEFAULT_COOLDOWN: 300000;
|
|
195
|
+
readonly MAX_ALERTS_PER_HOUR: 10;
|
|
196
|
+
readonly TYPES: {
|
|
197
|
+
readonly ODDS_DROP: "odds_drop";
|
|
198
|
+
readonly ODDS_VALUE: "odds_value";
|
|
199
|
+
readonly GOAL: "goal";
|
|
200
|
+
readonly RED_CARD: "red_card";
|
|
201
|
+
readonly LINEUP_CHANGE: "lineup_change";
|
|
202
|
+
readonly MATCH_START: "match_start";
|
|
203
|
+
readonly MATCH_END: "match_end";
|
|
204
|
+
readonly CUSTOM: "custom";
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
export declare const REALTIME_CONFIG: {
|
|
208
|
+
readonly WS_RECONNECT_INTERVAL: 5000;
|
|
209
|
+
readonly WS_MAX_RECONNECT_ATTEMPTS: 10;
|
|
210
|
+
readonly POLLING_INTERVALS: {
|
|
211
|
+
readonly LIVE_SCORES: 15000;
|
|
212
|
+
readonly ODDS: 30000;
|
|
213
|
+
readonly MATCH_EVENTS: 5000;
|
|
214
|
+
};
|
|
215
|
+
readonly EVENT_BATCH_SIZE: 10;
|
|
216
|
+
readonly EVENT_BATCH_TIMEOUT: 1000;
|
|
217
|
+
};
|
|
218
|
+
export declare const ML_CONFIG: {
|
|
219
|
+
readonly MIN_TRAINING_MATCHES: 50;
|
|
220
|
+
readonly FEATURE_WEIGHTS: {
|
|
221
|
+
readonly form: 0.25;
|
|
222
|
+
readonly h2h: 0.2;
|
|
223
|
+
readonly home_advantage: 0.15;
|
|
224
|
+
readonly xg: 0.15;
|
|
225
|
+
readonly injuries: 0.1;
|
|
226
|
+
readonly fatigue: 0.1;
|
|
227
|
+
readonly weather: 0.05;
|
|
228
|
+
};
|
|
229
|
+
readonly CONFIDENCE_THRESHOLDS: {
|
|
230
|
+
readonly high: 0.7;
|
|
231
|
+
readonly medium: 0.55;
|
|
232
|
+
readonly low: 0.4;
|
|
233
|
+
};
|
|
175
234
|
};
|
|
176
235
|
export declare const MODULE_INFO: {
|
|
177
236
|
readonly NAME: "sports-module";
|
|
178
|
-
readonly VERSION: "
|
|
179
|
-
readonly DESCRIPTION: "Football Intelligence System for MCP Server";
|
|
237
|
+
readonly VERSION: "3.0.0";
|
|
238
|
+
readonly DESCRIPTION: "Football Intelligence System for MCP Server - Realtime Edition";
|
|
180
239
|
};
|
|
@@ -14,31 +14,49 @@ export const API_CONFIG = {
|
|
|
14
14
|
// TheSportsDB
|
|
15
15
|
SPORTS_DB_KEY: process.env.SPORTS_DB_KEY || '',
|
|
16
16
|
SPORTS_DB_BASE: 'https://www.thesportsdb.com/api/v1/json',
|
|
17
|
+
// Odds API
|
|
18
|
+
ODDS_API_KEY: process.env.ODDS_API_KEY || '',
|
|
19
|
+
ODDS_API_BASE: 'https://api.the-odds-api.com/v4',
|
|
20
|
+
// SportRadar (requires enterprise license)
|
|
21
|
+
SPORTRADAR_KEY: process.env.SPORTRADAR_KEY || '',
|
|
22
|
+
SPORTRADAR_BASE: 'https://api.sportradar.us/soccer/trial/v4',
|
|
17
23
|
// Rate limits (requests per minute)
|
|
18
24
|
RATE_LIMITS: {
|
|
19
25
|
API_FOOTBALL: 10,
|
|
20
26
|
FOOTBALL_DATA: 10,
|
|
21
27
|
SPORTS_DB: 30,
|
|
28
|
+
ODDS_API: 20,
|
|
29
|
+
SPORTRADAR: 100,
|
|
22
30
|
}
|
|
23
31
|
};
|
|
24
32
|
// ============= Cache Configuration =============
|
|
25
33
|
export const CACHE_CONFIG = {
|
|
26
34
|
// Default TTL in milliseconds
|
|
27
35
|
DEFAULT_TTL: parseInt(process.env.SPORTS_CACHE_TTL || '300000'), // 5 minutes
|
|
28
|
-
// Specific TTLs for different data types
|
|
36
|
+
// Specific TTLs for different data types (OPTIMIZED FOR REALTIME)
|
|
29
37
|
TTL: {
|
|
30
|
-
LIVE_SCORES:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
LIVE_SCORES: 15 * 1000, // 15 seconds (was 1 minute)
|
|
39
|
+
LIVE_EVENTS: 5 * 1000, // 5 seconds for live match events
|
|
40
|
+
ODDS: 30 * 1000, // 30 seconds (was 2 minutes)
|
|
41
|
+
MATCH_DETAILS: 2 * 60 * 1000, // 2 minutes (was 5 minutes)
|
|
42
|
+
STANDINGS: 15 * 60 * 1000, // 15 minutes (was 30 minutes)
|
|
43
|
+
TEAM_STATS: 30 * 60 * 1000, // 30 minutes (was 1 hour)
|
|
44
|
+
PLAYER_STATS: 30 * 60 * 1000, // 30 minutes (was 1 hour)
|
|
45
|
+
TRANSFER_NEWS: 10 * 60 * 1000, // 10 minutes (was 15 minutes)
|
|
46
|
+
PREDICTIONS: 60 * 60 * 1000, // 1 hour
|
|
47
|
+
},
|
|
48
|
+
// Stale-while-revalidate multipliers
|
|
49
|
+
STALE_MULTIPLIERS: {
|
|
50
|
+
LIVE_SCORES: 2, // Stale for 30 seconds (15s * 2)
|
|
51
|
+
ODDS: 2, // Stale for 1 minute (30s * 2)
|
|
52
|
+
MATCH_DETAILS: 2, // Stale for 4 minutes
|
|
53
|
+
STANDINGS: 1.5, // Stale for 22.5 minutes
|
|
54
|
+
TEAM_STATS: 1.5, // Stale for 45 minutes
|
|
37
55
|
},
|
|
38
56
|
// Cache file path
|
|
39
57
|
CACHE_PATH: process.env.SPORTS_CACHE_PATH || '.sports_cache.json',
|
|
40
58
|
// Max cache size (number of entries)
|
|
41
|
-
MAX_SIZE:
|
|
59
|
+
MAX_SIZE: 2000,
|
|
42
60
|
};
|
|
43
61
|
// ============= Scraping Configuration =============
|
|
44
62
|
export const SCRAPER_CONFIG = {
|
|
@@ -152,10 +170,67 @@ export const ERRORS = {
|
|
|
152
170
|
TIMEOUT: 'Request timed out.',
|
|
153
171
|
INVALID_RESPONSE: 'Invalid response from API.',
|
|
154
172
|
ALL_PROVIDERS_FAILED: 'All data providers failed.',
|
|
173
|
+
CIRCUIT_BREAKER_OPEN: 'Service temporarily unavailable due to repeated failures.',
|
|
174
|
+
};
|
|
175
|
+
// ============= Alert Configuration =============
|
|
176
|
+
export const ALERT_CONFIG = {
|
|
177
|
+
// Check interval for alerts (milliseconds)
|
|
178
|
+
CHECK_INTERVAL: 10000, // 10 seconds
|
|
179
|
+
// Default cooldown between same alert (milliseconds)
|
|
180
|
+
DEFAULT_COOLDOWN: 300000, // 5 minutes
|
|
181
|
+
// Max alerts per hour per rule
|
|
182
|
+
MAX_ALERTS_PER_HOUR: 10,
|
|
183
|
+
// Alert types
|
|
184
|
+
TYPES: {
|
|
185
|
+
ODDS_DROP: 'odds_drop',
|
|
186
|
+
ODDS_VALUE: 'odds_value',
|
|
187
|
+
GOAL: 'goal',
|
|
188
|
+
RED_CARD: 'red_card',
|
|
189
|
+
LINEUP_CHANGE: 'lineup_change',
|
|
190
|
+
MATCH_START: 'match_start',
|
|
191
|
+
MATCH_END: 'match_end',
|
|
192
|
+
CUSTOM: 'custom',
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
// ============= Realtime Configuration =============
|
|
196
|
+
export const REALTIME_CONFIG = {
|
|
197
|
+
// WebSocket reconnection
|
|
198
|
+
WS_RECONNECT_INTERVAL: 5000,
|
|
199
|
+
WS_MAX_RECONNECT_ATTEMPTS: 10,
|
|
200
|
+
// Polling intervals (fallback when WebSocket unavailable)
|
|
201
|
+
POLLING_INTERVALS: {
|
|
202
|
+
LIVE_SCORES: 15000, // 15 seconds
|
|
203
|
+
ODDS: 30000, // 30 seconds
|
|
204
|
+
MATCH_EVENTS: 5000, // 5 seconds
|
|
205
|
+
},
|
|
206
|
+
// Event batching
|
|
207
|
+
EVENT_BATCH_SIZE: 10,
|
|
208
|
+
EVENT_BATCH_TIMEOUT: 1000,
|
|
209
|
+
};
|
|
210
|
+
// ============= ML Prediction Configuration =============
|
|
211
|
+
export const ML_CONFIG = {
|
|
212
|
+
// Minimum matches for training
|
|
213
|
+
MIN_TRAINING_MATCHES: 50,
|
|
214
|
+
// Feature weights
|
|
215
|
+
FEATURE_WEIGHTS: {
|
|
216
|
+
form: 0.25,
|
|
217
|
+
h2h: 0.20,
|
|
218
|
+
home_advantage: 0.15,
|
|
219
|
+
xg: 0.15,
|
|
220
|
+
injuries: 0.10,
|
|
221
|
+
fatigue: 0.10,
|
|
222
|
+
weather: 0.05,
|
|
223
|
+
},
|
|
224
|
+
// Confidence thresholds
|
|
225
|
+
CONFIDENCE_THRESHOLDS: {
|
|
226
|
+
high: 0.70,
|
|
227
|
+
medium: 0.55,
|
|
228
|
+
low: 0.40,
|
|
229
|
+
},
|
|
155
230
|
};
|
|
156
231
|
// ============= Version Info =============
|
|
157
232
|
export const MODULE_INFO = {
|
|
158
233
|
NAME: 'sports-module',
|
|
159
|
-
VERSION: '
|
|
160
|
-
DESCRIPTION: 'Football Intelligence System for MCP Server',
|
|
234
|
+
VERSION: '3.0.0',
|
|
235
|
+
DESCRIPTION: 'Football Intelligence System for MCP Server - Realtime Edition',
|
|
161
236
|
};
|