@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.
@@ -0,0 +1,331 @@
1
+ /**
2
+ * REALTIME DATA MANAGER
3
+ * WebSocket/SSE based real-time data streaming with fallback polling
4
+ */
5
+ import { EventEmitter } from 'events';
6
+ import { CacheService } from './cache.js';
7
+ import { logger } from '../../../utils.js';
8
+ import { REALTIME_CONFIG, CACHE_CONFIG } from './constants.js';
9
+ export class RealtimeDataManager extends EventEmitter {
10
+ cache;
11
+ wsConnections = new Map();
12
+ pollingIntervals = new Map();
13
+ reconnectAttempts = new Map();
14
+ isRunning = false;
15
+ constructor() {
16
+ super();
17
+ this.cache = new CacheService();
18
+ this.setMaxListeners(100);
19
+ }
20
+ /**
21
+ * Start the realtime manager
22
+ */
23
+ async start() {
24
+ if (this.isRunning)
25
+ return;
26
+ this.isRunning = true;
27
+ logger.info('[RealtimeManager] Started');
28
+ // Start polling for live data
29
+ this.startLiveScoresPolling();
30
+ this.startOddsPolling();
31
+ }
32
+ /**
33
+ * Stop the realtime manager
34
+ */
35
+ stop() {
36
+ this.isRunning = false;
37
+ // Close all WebSocket connections
38
+ for (const [matchId, ws] of this.wsConnections.entries()) {
39
+ ws.close();
40
+ }
41
+ this.wsConnections.clear();
42
+ // Clear all polling intervals
43
+ for (const [key, interval] of this.pollingIntervals.entries()) {
44
+ clearInterval(interval);
45
+ }
46
+ this.pollingIntervals.clear();
47
+ logger.info('[RealtimeManager] Stopped');
48
+ }
49
+ /**
50
+ * Subscribe to a specific match's events
51
+ */
52
+ subscribeToMatch(matchId, callback) {
53
+ const eventName = `match:${matchId}`;
54
+ this.on(eventName, callback);
55
+ // Try to establish WebSocket connection if available
56
+ this.connectToMatchStream(matchId);
57
+ // Return unsubscribe function
58
+ return () => {
59
+ this.off(eventName, callback);
60
+ this.unsubscribeFromMatch(matchId);
61
+ };
62
+ }
63
+ /**
64
+ * Subscribe to odds changes for a match
65
+ */
66
+ subscribeToOdds(matchId, callback) {
67
+ const eventName = `odds:${matchId}`;
68
+ this.on(eventName, callback);
69
+ return () => {
70
+ this.off(eventName, callback);
71
+ };
72
+ }
73
+ /**
74
+ * Subscribe to all live events
75
+ */
76
+ subscribeToAllEvents(callback) {
77
+ this.on('event', callback);
78
+ return () => this.off('event', callback);
79
+ }
80
+ /**
81
+ * Process and broadcast an event
82
+ */
83
+ processEvent(event) {
84
+ // Update cache
85
+ this.updateCache(event);
86
+ // Emit to specific match listeners
87
+ this.emit(`match:${event.matchId}`, event);
88
+ // Emit to odds listeners if it's an odds change
89
+ if (event.type === 'odds_change') {
90
+ this.emit(`odds:${event.matchId}`, event);
91
+ }
92
+ // Emit to global listeners
93
+ this.emit('event', event);
94
+ // Log event
95
+ logger.debug(`[Realtime] Event: ${event.type} | Match: ${event.matchId} | Min: ${event.minute}`);
96
+ }
97
+ /**
98
+ * Get current live matches
99
+ */
100
+ getLiveMatches() {
101
+ const cached = this.cache.get('live:all');
102
+ return cached || [];
103
+ }
104
+ /**
105
+ * Get match events history
106
+ */
107
+ getMatchEvents(matchId) {
108
+ const cached = this.cache.get(`events:${matchId}`);
109
+ return cached || [];
110
+ }
111
+ /**
112
+ * Connect to match stream (WebSocket or polling)
113
+ */
114
+ async connectToMatchStream(matchId) {
115
+ // For now, use polling fallback
116
+ // In production, this would try WebSocket first
117
+ this.startMatchEventsPolling(matchId);
118
+ }
119
+ /**
120
+ * Unsubscribe from a match
121
+ */
122
+ unsubscribeFromMatch(matchId) {
123
+ // Close WebSocket if exists
124
+ const ws = this.wsConnections.get(matchId);
125
+ if (ws) {
126
+ ws.close();
127
+ this.wsConnections.delete(matchId);
128
+ }
129
+ // Clear polling if no more listeners
130
+ const listenerCount = this.listenerCount(`match:${matchId}`);
131
+ if (listenerCount === 0) {
132
+ const interval = this.pollingIntervals.get(`events:${matchId}`);
133
+ if (interval) {
134
+ clearInterval(interval);
135
+ this.pollingIntervals.delete(`events:${matchId}`);
136
+ }
137
+ }
138
+ }
139
+ /**
140
+ * Start polling for live scores
141
+ */
142
+ startLiveScoresPolling() {
143
+ const interval = setInterval(async () => {
144
+ if (!this.isRunning)
145
+ return;
146
+ try {
147
+ // Fetch live scores from API
148
+ // This would call your API provider
149
+ const liveScores = await this.fetchLiveScores();
150
+ // Update cache
151
+ this.cache.set('live:all', liveScores, CACHE_CONFIG.TTL.LIVE_SCORES);
152
+ // Emit update
153
+ this.emit('live_scores', liveScores);
154
+ // Check for score changes and emit events
155
+ this.detectScoreChanges(liveScores);
156
+ }
157
+ catch (error) {
158
+ logger.error('[Realtime] Failed to fetch live scores:', error);
159
+ }
160
+ }, REALTIME_CONFIG.POLLING_INTERVALS.LIVE_SCORES);
161
+ this.pollingIntervals.set('live_scores', interval);
162
+ }
163
+ /**
164
+ * Start polling for odds
165
+ */
166
+ startOddsPolling() {
167
+ const interval = setInterval(async () => {
168
+ if (!this.isRunning)
169
+ return;
170
+ try {
171
+ const liveMatches = this.getLiveMatches();
172
+ for (const match of liveMatches) {
173
+ const odds = await this.fetchOdds(match.id);
174
+ if (odds) {
175
+ const cachedOdds = this.cache.get(`odds:${match.id}`);
176
+ if (cachedOdds) {
177
+ // Detect changes
178
+ this.detectOddsChanges(match.id, cachedOdds, odds);
179
+ }
180
+ this.cache.set(`odds:${match.id}`, odds, CACHE_CONFIG.TTL.ODDS);
181
+ }
182
+ }
183
+ }
184
+ catch (error) {
185
+ logger.error('[Realtime] Failed to fetch odds:', error);
186
+ }
187
+ }, REALTIME_CONFIG.POLLING_INTERVALS.ODDS);
188
+ this.pollingIntervals.set('odds', interval);
189
+ }
190
+ /**
191
+ * Start polling for specific match events
192
+ */
193
+ startMatchEventsPolling(matchId) {
194
+ if (this.pollingIntervals.has(`events:${matchId}`))
195
+ return;
196
+ const interval = setInterval(async () => {
197
+ if (!this.isRunning)
198
+ return;
199
+ try {
200
+ const events = await this.fetchMatchEvents(matchId);
201
+ const cachedEvents = this.getMatchEvents(matchId);
202
+ // Find new events
203
+ const newEvents = events.filter(e => !cachedEvents.some(ce => ce.timestamp === e.timestamp && ce.type === e.type));
204
+ for (const event of newEvents) {
205
+ this.processEvent(event);
206
+ }
207
+ }
208
+ catch (error) {
209
+ logger.error(`[Realtime] Failed to fetch events for match ${matchId}:`, error);
210
+ }
211
+ }, REALTIME_CONFIG.POLLING_INTERVALS.MATCH_EVENTS);
212
+ this.pollingIntervals.set(`events:${matchId}`, interval);
213
+ }
214
+ /**
215
+ * Detect score changes and emit goal events
216
+ */
217
+ detectScoreChanges(matches) {
218
+ for (const match of matches) {
219
+ const cachedMatch = this.cache.get(`match:${match.id}`);
220
+ if (cachedMatch && cachedMatch.score && match.score) {
221
+ // Check for home team goal
222
+ if (match.score.home > cachedMatch.score.home) {
223
+ this.processEvent({
224
+ type: 'goal',
225
+ matchId: match.id,
226
+ timestamp: Date.now(),
227
+ minute: match.minute,
228
+ data: {
229
+ team: 'home',
230
+ score: match.score,
231
+ previousScore: cachedMatch.score,
232
+ },
233
+ });
234
+ }
235
+ // Check for away team goal
236
+ if (match.score.away > cachedMatch.score.away) {
237
+ this.processEvent({
238
+ type: 'goal',
239
+ matchId: match.id,
240
+ timestamp: Date.now(),
241
+ minute: match.minute,
242
+ data: {
243
+ team: 'away',
244
+ score: match.score,
245
+ previousScore: cachedMatch.score,
246
+ },
247
+ });
248
+ }
249
+ }
250
+ // Update match cache
251
+ this.cache.set(`match:${match.id}`, match, CACHE_CONFIG.TTL.MATCH_DETAILS);
252
+ }
253
+ }
254
+ /**
255
+ * Detect odds changes and emit events
256
+ */
257
+ detectOddsChanges(matchId, oldOdds, newOdds) {
258
+ const markets = [
259
+ { key: 'homeWin', name: 'Home Win' },
260
+ { key: 'draw', name: 'Draw' },
261
+ { key: 'awayWin', name: 'Away Win' },
262
+ ];
263
+ for (const market of markets) {
264
+ const oldValue = oldOdds[market.key];
265
+ const newValue = newOdds[market.key];
266
+ if (oldValue && newValue && oldValue !== newValue) {
267
+ const change = ((newValue - oldValue) / oldValue) * 100;
268
+ // Only emit if change is significant (>5%)
269
+ if (Math.abs(change) > 5) {
270
+ this.processEvent({
271
+ type: 'odds_change',
272
+ matchId,
273
+ timestamp: Date.now(),
274
+ data: {
275
+ bookmaker: newOdds.provider,
276
+ market: market.name,
277
+ selection: market.key,
278
+ oldOdds: oldValue,
279
+ newOdds: newValue,
280
+ change,
281
+ timestamp: Date.now(),
282
+ },
283
+ });
284
+ }
285
+ }
286
+ }
287
+ }
288
+ /**
289
+ * Update cache with event
290
+ */
291
+ updateCache(event) {
292
+ // Invalidate related cache entries
293
+ this.cache.invalidatePattern(`match:${event.matchId}*`);
294
+ // Store event in history
295
+ const eventsKey = `events:${event.matchId}`;
296
+ const existingEvents = this.cache.get(eventsKey) || [];
297
+ existingEvents.push(event);
298
+ // Keep only last 100 events
299
+ if (existingEvents.length > 100) {
300
+ existingEvents.shift();
301
+ }
302
+ this.cache.set(eventsKey, existingEvents, CACHE_CONFIG.TTL.LIVE_EVENTS);
303
+ }
304
+ // Placeholder methods - implement with actual API calls
305
+ async fetchLiveScores() {
306
+ // Implement with your API provider
307
+ return [];
308
+ }
309
+ async fetchOdds(matchId) {
310
+ // Implement with your API provider
311
+ return null;
312
+ }
313
+ async fetchMatchEvents(matchId) {
314
+ // Implement with your API provider
315
+ return [];
316
+ }
317
+ }
318
+ // Singleton instance
319
+ let globalRealtimeManager = null;
320
+ export function getRealtimeManager() {
321
+ if (!globalRealtimeManager) {
322
+ globalRealtimeManager = new RealtimeDataManager();
323
+ }
324
+ return globalRealtimeManager;
325
+ }
326
+ export function resetRealtimeManager() {
327
+ if (globalRealtimeManager) {
328
+ globalRealtimeManager.stop();
329
+ }
330
+ globalRealtimeManager = null;
331
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * RETRY MECHANISM
3
+ * Exponential backoff with jitter for resilient API calls
4
+ */
5
+ export interface RetryConfig {
6
+ maxAttempts: number;
7
+ initialDelay: number;
8
+ maxDelay: number;
9
+ backoffMultiplier: number;
10
+ retryableErrors?: string[];
11
+ onRetry?: (attempt: number, error: Error, delay: number) => void;
12
+ }
13
+ export declare const DEFAULT_RETRY_CONFIG: RetryConfig;
14
+ export declare const AGGRESSIVE_RETRY_CONFIG: RetryConfig;
15
+ export declare const LIVE_DATA_RETRY_CONFIG: RetryConfig;
16
+ /**
17
+ * Execute a function with retry logic
18
+ */
19
+ export declare function withRetry<T>(fn: () => Promise<T>, config?: Partial<RetryConfig>, context?: string): Promise<T>;
20
+ /**
21
+ * Retry with circuit breaker combination
22
+ */
23
+ export declare function withRetryAndCircuitBreaker<T>(fn: () => Promise<T>, circuitBreaker: {
24
+ execute: <U>(fn: () => Promise<U>) => Promise<U>;
25
+ }, config?: Partial<RetryConfig>, context?: string): Promise<T>;
26
+ /**
27
+ * Create a retryable wrapper for a function
28
+ */
29
+ export declare function createRetryable<T extends (...args: any[]) => Promise<any>>(fn: T, config?: Partial<RetryConfig>, context?: string): T;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * RETRY MECHANISM
3
+ * Exponential backoff with jitter for resilient API calls
4
+ */
5
+ import { logger } from '../../../utils.js';
6
+ export const DEFAULT_RETRY_CONFIG = {
7
+ maxAttempts: 3,
8
+ initialDelay: 1000,
9
+ maxDelay: 30000,
10
+ backoffMultiplier: 2,
11
+ retryableErrors: ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', '503', '502', '504', '429'],
12
+ };
13
+ export const AGGRESSIVE_RETRY_CONFIG = {
14
+ maxAttempts: 5,
15
+ initialDelay: 500,
16
+ maxDelay: 60000,
17
+ backoffMultiplier: 2,
18
+ retryableErrors: ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', '503', '502', '504', '429', '500'],
19
+ };
20
+ export const LIVE_DATA_RETRY_CONFIG = {
21
+ maxAttempts: 3,
22
+ initialDelay: 200,
23
+ maxDelay: 5000,
24
+ backoffMultiplier: 1.5,
25
+ retryableErrors: ['ECONNRESET', 'ETIMEDOUT', '503', '504'],
26
+ };
27
+ function sleep(ms) {
28
+ return new Promise(resolve => setTimeout(resolve, ms));
29
+ }
30
+ /**
31
+ * Execute a function with retry logic
32
+ */
33
+ export async function withRetry(fn, config = {}, context) {
34
+ const fullConfig = { ...DEFAULT_RETRY_CONFIG, ...config };
35
+ let lastError = null;
36
+ for (let attempt = 1; attempt <= fullConfig.maxAttempts; attempt++) {
37
+ try {
38
+ return await fn();
39
+ }
40
+ catch (error) {
41
+ lastError = error instanceof Error ? error : new Error(String(error));
42
+ // Check if error is retryable
43
+ if (fullConfig.retryableErrors && !fullConfig.retryableErrors.some(e => lastError.message.includes(e) ||
44
+ lastError.code?.includes(e))) {
45
+ throw lastError;
46
+ }
47
+ if (attempt === fullConfig.maxAttempts) {
48
+ break;
49
+ }
50
+ // Calculate delay with exponential backoff and jitter
51
+ const delay = Math.min(fullConfig.initialDelay * Math.pow(fullConfig.backoffMultiplier, attempt - 1), fullConfig.maxDelay);
52
+ const jitteredDelay = delay * (0.8 + Math.random() * 0.4); // ±20% jitter
53
+ const contextStr = context ? `[${context}]` : '';
54
+ logger.warn(`[Retry${contextStr}] Attempt ${attempt}/${fullConfig.maxAttempts} failed: ${lastError.message}. Retrying in ${Math.round(jitteredDelay)}ms...`);
55
+ if (fullConfig.onRetry) {
56
+ fullConfig.onRetry(attempt, lastError, jitteredDelay);
57
+ }
58
+ await sleep(jitteredDelay);
59
+ }
60
+ }
61
+ const contextStr = context ? `[${context}]` : '';
62
+ throw new Error(`[Retry${contextStr}] All ${fullConfig.maxAttempts} attempts failed. Last error: ${lastError?.message}`);
63
+ }
64
+ /**
65
+ * Retry with circuit breaker combination
66
+ */
67
+ export async function withRetryAndCircuitBreaker(fn, circuitBreaker, config, context) {
68
+ return circuitBreaker.execute(() => withRetry(fn, config, context));
69
+ }
70
+ /**
71
+ * Create a retryable wrapper for a function
72
+ */
73
+ export function createRetryable(fn, config, context) {
74
+ return (async (...args) => {
75
+ return withRetry(() => fn(...args), config, context);
76
+ });
77
+ }
@@ -7,7 +7,7 @@ export type MatchStatus = 'scheduled' | 'live' | 'finished' | 'postponed' | 'can
7
7
  export type BetType = 'home' | 'draw' | 'away' | 'over' | 'under' | 'btts' | 'handicap';
8
8
  export type KellyVariant = 'full' | 'half' | 'quarter';
9
9
  export type QueryType = 'general' | 'h2h' | 'form' | 'news' | 'stats' | 'odds' | 'referee' | 'weather' | 'fatigue' | 'setpieces';
10
- export type ProviderType = 'api-football' | 'football-data' | 'sports-db' | 'scraper';
10
+ export type ProviderType = 'api-football' | 'football-data' | 'sports-db' | 'odds-api' | 'sportradar' | 'scraper';
11
11
  export interface SearchQuery {
12
12
  type: QueryType;
13
13
  query: string;
@@ -175,6 +175,8 @@ export interface Match {
175
175
  homeSubstitutes?: Player[];
176
176
  awaySubstitutes?: Player[];
177
177
  events?: MatchEvent[];
178
+ dataQuality?: number;
179
+ lastUpdated?: Date;
178
180
  }
179
181
  export interface MatchOdds {
180
182
  homeWin: number;
@@ -344,6 +346,7 @@ export interface APIResponse<T = any> {
344
346
  provider?: ProviderType;
345
347
  cached?: boolean;
346
348
  rateLimited?: boolean;
349
+ quality?: number;
347
350
  }
348
351
  export interface ProviderStatus {
349
352
  name: ProviderType;
@@ -352,6 +355,7 @@ export interface ProviderStatus {
352
355
  quotaLimit: number;
353
356
  rateLimitUntil?: Date;
354
357
  lastCall?: Date;
358
+ circuitBreakerState?: 'closed' | 'open' | 'half-open';
355
359
  }
356
360
  export interface ComparisonResult {
357
361
  teamA: Team;
@@ -377,3 +381,38 @@ export interface LiveScoreUpdate {
377
381
  events: MatchEvent[];
378
382
  timestamp: Date;
379
383
  }
384
+ export interface MLPrediction {
385
+ matchId: string;
386
+ homeWinProbability: number;
387
+ drawProbability: number;
388
+ awayWinProbability: number;
389
+ over25Probability?: number;
390
+ bttsProbability?: number;
391
+ confidence: number;
392
+ factors: PredictionFactor[];
393
+ timestamp: Date;
394
+ }
395
+ export interface PredictionFactor {
396
+ name: string;
397
+ weight: number;
398
+ impact: 'positive' | 'negative' | 'neutral';
399
+ description: string;
400
+ }
401
+ export interface HistoricalPattern {
402
+ pattern: string;
403
+ occurrence: number;
404
+ successRate: number;
405
+ avgOdds: number;
406
+ profit: number;
407
+ }
408
+ export interface TeamHistoricalPerformance {
409
+ team: Team;
410
+ totalMatches: number;
411
+ wins: number;
412
+ draws: number;
413
+ losses: number;
414
+ avgGoalsFor: number;
415
+ avgGoalsAgainst: number;
416
+ homeAdvantage: number;
417
+ patterns: HistoricalPattern[];
418
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotza02/sequential-thinking",
3
- "version": "10000.0.8",
3
+ "version": "10000.1.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },