@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
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ALERT MANAGER
|
|
3
|
+
* Real-time alert system with rule engine and notifications
|
|
4
|
+
*/
|
|
5
|
+
import { EventEmitter } from 'events';
|
|
6
|
+
import { logger } from '../../../utils.js';
|
|
7
|
+
import { ALERT_CONFIG } from './constants.js';
|
|
8
|
+
export class AlertManager extends EventEmitter {
|
|
9
|
+
rules = new Map();
|
|
10
|
+
checkInterval;
|
|
11
|
+
alertHistory = [];
|
|
12
|
+
maxHistorySize = 1000;
|
|
13
|
+
constructor() {
|
|
14
|
+
super();
|
|
15
|
+
this.setMaxListeners(100);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Start the alert manager
|
|
19
|
+
*/
|
|
20
|
+
start() {
|
|
21
|
+
if (this.checkInterval)
|
|
22
|
+
return;
|
|
23
|
+
this.checkInterval = setInterval(() => {
|
|
24
|
+
this.checkScheduledAlerts();
|
|
25
|
+
}, ALERT_CONFIG.CHECK_INTERVAL);
|
|
26
|
+
logger.info('[AlertManager] Started');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Stop the alert manager
|
|
30
|
+
*/
|
|
31
|
+
stop() {
|
|
32
|
+
if (this.checkInterval) {
|
|
33
|
+
clearInterval(this.checkInterval);
|
|
34
|
+
this.checkInterval = undefined;
|
|
35
|
+
}
|
|
36
|
+
logger.info('[AlertManager] Stopped');
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Add a new alert rule
|
|
40
|
+
*/
|
|
41
|
+
addRule(rule) {
|
|
42
|
+
const newRule = {
|
|
43
|
+
...rule,
|
|
44
|
+
id: `alert-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
45
|
+
createdAt: Date.now(),
|
|
46
|
+
triggerCount: 0,
|
|
47
|
+
};
|
|
48
|
+
this.rules.set(newRule.id, newRule);
|
|
49
|
+
logger.info(`[AlertManager] Added rule: ${newRule.id} (${newRule.name})`);
|
|
50
|
+
this.emit('rule_added', newRule);
|
|
51
|
+
return newRule;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Remove an alert rule
|
|
55
|
+
*/
|
|
56
|
+
removeRule(ruleId) {
|
|
57
|
+
const removed = this.rules.delete(ruleId);
|
|
58
|
+
if (removed) {
|
|
59
|
+
logger.info(`[AlertManager] Removed rule: ${ruleId}`);
|
|
60
|
+
this.emit('rule_removed', ruleId);
|
|
61
|
+
}
|
|
62
|
+
return removed;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get all rules
|
|
66
|
+
*/
|
|
67
|
+
getRules() {
|
|
68
|
+
return Array.from(this.rules.values());
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Get a specific rule
|
|
72
|
+
*/
|
|
73
|
+
getRule(ruleId) {
|
|
74
|
+
return this.rules.get(ruleId);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Enable/disable a rule
|
|
78
|
+
*/
|
|
79
|
+
toggleRule(ruleId, enabled) {
|
|
80
|
+
const rule = this.rules.get(ruleId);
|
|
81
|
+
if (rule) {
|
|
82
|
+
rule.enabled = enabled;
|
|
83
|
+
logger.info(`[AlertManager] Rule ${ruleId} ${enabled ? 'enabled' : 'disabled'}`);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Process a live event and check for alerts
|
|
90
|
+
*/
|
|
91
|
+
processEvent(event) {
|
|
92
|
+
for (const rule of this.rules.values()) {
|
|
93
|
+
if (!rule.enabled)
|
|
94
|
+
continue;
|
|
95
|
+
// Check cooldown
|
|
96
|
+
if (rule.lastTriggered && Date.now() - rule.lastTriggered < rule.cooldown) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
// Check rate limit
|
|
100
|
+
if (rule.triggerCount && rule.triggerCount >= ALERT_CONFIG.MAX_ALERTS_PER_HOUR) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (this.evaluateCondition(rule.condition, event)) {
|
|
104
|
+
this.triggerAlert(rule, event);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Evaluate an alert condition against an event
|
|
110
|
+
*/
|
|
111
|
+
evaluateCondition(condition, event) {
|
|
112
|
+
switch (condition.type) {
|
|
113
|
+
case 'odds_drop':
|
|
114
|
+
return this.evaluateOddsDrop(condition, event);
|
|
115
|
+
case 'odds_value':
|
|
116
|
+
return this.evaluateOddsValue(condition, event);
|
|
117
|
+
case 'event':
|
|
118
|
+
return this.evaluateEventCondition(condition, event);
|
|
119
|
+
case 'composite':
|
|
120
|
+
return this.evaluateCompositeCondition(condition, event);
|
|
121
|
+
default:
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
evaluateOddsDrop(condition, event) {
|
|
126
|
+
if (event.type !== 'odds_change')
|
|
127
|
+
return false;
|
|
128
|
+
const oddsEvent = event;
|
|
129
|
+
const { newOdds, change } = oddsEvent.data;
|
|
130
|
+
// Check match filter
|
|
131
|
+
if (condition.matchId && event.matchId !== condition.matchId)
|
|
132
|
+
return false;
|
|
133
|
+
// Check threshold
|
|
134
|
+
if (newOdds > condition.threshold)
|
|
135
|
+
return false;
|
|
136
|
+
// Check percentage drop
|
|
137
|
+
return Math.abs(change) >= condition.percentage;
|
|
138
|
+
}
|
|
139
|
+
evaluateOddsValue(condition, event) {
|
|
140
|
+
if (event.type !== 'odds_change')
|
|
141
|
+
return false;
|
|
142
|
+
const oddsEvent = event;
|
|
143
|
+
const { newOdds } = oddsEvent.data;
|
|
144
|
+
// Check match filter
|
|
145
|
+
if (condition.matchId && event.matchId !== condition.matchId)
|
|
146
|
+
return false;
|
|
147
|
+
// Check max odds
|
|
148
|
+
if (condition.maxOdds && newOdds > condition.maxOdds)
|
|
149
|
+
return false;
|
|
150
|
+
// Calculate value (would need probability data)
|
|
151
|
+
// For now, simplified check
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
evaluateEventCondition(condition, event) {
|
|
155
|
+
// Check match filter
|
|
156
|
+
if (condition.matchId && event.matchId !== condition.matchId)
|
|
157
|
+
return false;
|
|
158
|
+
// Check event type
|
|
159
|
+
return condition.eventTypes.includes(event.type);
|
|
160
|
+
}
|
|
161
|
+
evaluateCompositeCondition(condition, event) {
|
|
162
|
+
const results = condition.conditions.map(c => this.evaluateCondition(c, event));
|
|
163
|
+
return condition.operator === 'AND'
|
|
164
|
+
? results.every(r => r)
|
|
165
|
+
: results.some(r => r);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Trigger an alert
|
|
169
|
+
*/
|
|
170
|
+
async triggerAlert(rule, event) {
|
|
171
|
+
rule.lastTriggered = Date.now();
|
|
172
|
+
rule.triggerCount = (rule.triggerCount || 0) + 1;
|
|
173
|
+
const message = this.createAlertMessage(rule, event);
|
|
174
|
+
// Store in history
|
|
175
|
+
this.alertHistory.push(message);
|
|
176
|
+
if (this.alertHistory.length > this.maxHistorySize) {
|
|
177
|
+
this.alertHistory.shift();
|
|
178
|
+
}
|
|
179
|
+
// Send notifications
|
|
180
|
+
for (const channel of rule.channels) {
|
|
181
|
+
try {
|
|
182
|
+
await this.sendNotification(channel, message);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
logger.error(`[AlertManager] Failed to send notification: ${error}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Emit event
|
|
189
|
+
this.emit('alert', message);
|
|
190
|
+
logger.info(`[AlertManager] Alert triggered: ${rule.name} (${rule.id})`);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Create an alert message
|
|
194
|
+
*/
|
|
195
|
+
createAlertMessage(rule, event) {
|
|
196
|
+
const severity = this.determineSeverity(rule.type, event);
|
|
197
|
+
return {
|
|
198
|
+
ruleId: rule.id,
|
|
199
|
+
ruleName: rule.name,
|
|
200
|
+
type: rule.type,
|
|
201
|
+
severity,
|
|
202
|
+
title: this.formatAlertTitle(rule, event),
|
|
203
|
+
body: this.formatAlertBody(rule, event),
|
|
204
|
+
data: {
|
|
205
|
+
event,
|
|
206
|
+
rule,
|
|
207
|
+
},
|
|
208
|
+
timestamp: Date.now(),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
determineSeverity(type, event) {
|
|
212
|
+
switch (type) {
|
|
213
|
+
case 'goal':
|
|
214
|
+
case 'red_card':
|
|
215
|
+
return 'critical';
|
|
216
|
+
case 'odds_drop':
|
|
217
|
+
case 'odds_value':
|
|
218
|
+
return 'warning';
|
|
219
|
+
default:
|
|
220
|
+
return 'info';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
formatAlertTitle(rule, event) {
|
|
224
|
+
switch (rule.type) {
|
|
225
|
+
case 'odds_drop':
|
|
226
|
+
return `🚨 Odds Drop Alert: ${event.matchId}`;
|
|
227
|
+
case 'odds_value':
|
|
228
|
+
return `💰 Value Bet Alert: ${event.matchId}`;
|
|
229
|
+
case 'goal':
|
|
230
|
+
return `⚽ GOAL! ${event.matchId}`;
|
|
231
|
+
case 'red_card':
|
|
232
|
+
return `🔴 RED CARD! ${event.matchId}`;
|
|
233
|
+
default:
|
|
234
|
+
return `📢 ${rule.name}`;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
formatAlertBody(rule, event) {
|
|
238
|
+
const lines = [];
|
|
239
|
+
lines.push(`Rule: ${rule.name}`);
|
|
240
|
+
lines.push(`Type: ${rule.type}`);
|
|
241
|
+
lines.push(`Match: ${event.matchId}`);
|
|
242
|
+
lines.push(`Time: ${new Date(event.timestamp).toLocaleString()}`);
|
|
243
|
+
if (event.minute) {
|
|
244
|
+
lines.push(`Minute: ${event.minute}'`);
|
|
245
|
+
}
|
|
246
|
+
if (event.data) {
|
|
247
|
+
lines.push('');
|
|
248
|
+
lines.push('Details:');
|
|
249
|
+
lines.push(JSON.stringify(event.data, null, 2));
|
|
250
|
+
}
|
|
251
|
+
return lines.join('\n');
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Send notification to a channel
|
|
255
|
+
*/
|
|
256
|
+
async sendNotification(channel, message) {
|
|
257
|
+
switch (channel.type) {
|
|
258
|
+
case 'webhook':
|
|
259
|
+
await this.sendWebhook(channel.config.url, message);
|
|
260
|
+
break;
|
|
261
|
+
case 'slack':
|
|
262
|
+
await this.sendSlack(channel.config.webhook, message);
|
|
263
|
+
break;
|
|
264
|
+
case 'discord':
|
|
265
|
+
await this.sendDiscord(channel.config.webhook, message);
|
|
266
|
+
break;
|
|
267
|
+
case 'email':
|
|
268
|
+
await this.sendEmail(channel.config, message);
|
|
269
|
+
break;
|
|
270
|
+
case 'console':
|
|
271
|
+
console.log(`\n${'='.repeat(50)}`);
|
|
272
|
+
console.log(`ALERT: ${message.title}`);
|
|
273
|
+
console.log(`${'='.repeat(50)}`);
|
|
274
|
+
console.log(message.body);
|
|
275
|
+
console.log(`${'='.repeat(50)}\n`);
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async sendWebhook(url, message) {
|
|
280
|
+
const response = await fetch(url, {
|
|
281
|
+
method: 'POST',
|
|
282
|
+
headers: { 'Content-Type': 'application/json' },
|
|
283
|
+
body: JSON.stringify(message),
|
|
284
|
+
});
|
|
285
|
+
if (!response.ok) {
|
|
286
|
+
throw new Error(`Webhook failed: ${response.status}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
async sendSlack(webhook, message) {
|
|
290
|
+
const emoji = message.severity === 'critical' ? '🚨' :
|
|
291
|
+
message.severity === 'warning' ? '⚠️' : 'ℹ️';
|
|
292
|
+
const payload = {
|
|
293
|
+
text: `${emoji} *${message.title}*`,
|
|
294
|
+
attachments: [{
|
|
295
|
+
color: message.severity === 'critical' ? 'danger' :
|
|
296
|
+
message.severity === 'warning' ? 'warning' : 'good',
|
|
297
|
+
text: message.body,
|
|
298
|
+
footer: 'Football Alert System',
|
|
299
|
+
ts: Math.floor(message.timestamp / 1000),
|
|
300
|
+
}],
|
|
301
|
+
};
|
|
302
|
+
const response = await fetch(webhook, {
|
|
303
|
+
method: 'POST',
|
|
304
|
+
headers: { 'Content-Type': 'application/json' },
|
|
305
|
+
body: JSON.stringify(payload),
|
|
306
|
+
});
|
|
307
|
+
if (!response.ok) {
|
|
308
|
+
throw new Error(`Slack webhook failed: ${response.status}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async sendDiscord(webhook, message) {
|
|
312
|
+
const color = message.severity === 'critical' ? 0xff0000 :
|
|
313
|
+
message.severity === 'warning' ? 0xffa500 : 0x00ff00;
|
|
314
|
+
const payload = {
|
|
315
|
+
embeds: [{
|
|
316
|
+
title: message.title,
|
|
317
|
+
description: message.body,
|
|
318
|
+
color: color,
|
|
319
|
+
timestamp: new Date(message.timestamp).toISOString(),
|
|
320
|
+
}],
|
|
321
|
+
};
|
|
322
|
+
const response = await fetch(webhook, {
|
|
323
|
+
method: 'POST',
|
|
324
|
+
headers: { 'Content-Type': 'application/json' },
|
|
325
|
+
body: JSON.stringify(payload),
|
|
326
|
+
});
|
|
327
|
+
if (!response.ok) {
|
|
328
|
+
throw new Error(`Discord webhook failed: ${response.status}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
async sendEmail(config, message) {
|
|
332
|
+
// Would integrate with email service like SendGrid, AWS SES, etc.
|
|
333
|
+
logger.info(`[AlertManager] Would send email to ${config.to}: ${message.title}`);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Check scheduled alerts (for time-based alerts)
|
|
337
|
+
*/
|
|
338
|
+
checkScheduledAlerts() {
|
|
339
|
+
// Implement time-based alert checking
|
|
340
|
+
// e.g., check for upcoming matches, etc.
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Get alert history
|
|
344
|
+
*/
|
|
345
|
+
getAlertHistory(limit) {
|
|
346
|
+
const history = [...this.alertHistory].reverse();
|
|
347
|
+
return limit ? history.slice(0, limit) : history;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Clear alert history
|
|
351
|
+
*/
|
|
352
|
+
clearHistory() {
|
|
353
|
+
this.alertHistory = [];
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Get statistics
|
|
357
|
+
*/
|
|
358
|
+
getStats() {
|
|
359
|
+
const rules = Array.from(this.rules.values());
|
|
360
|
+
return {
|
|
361
|
+
totalRules: rules.length,
|
|
362
|
+
enabledRules: rules.filter(r => r.enabled).length,
|
|
363
|
+
totalAlerts: this.alertHistory.length,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
// Singleton instance
|
|
368
|
+
let globalAlertManager = null;
|
|
369
|
+
export function getAlertManager() {
|
|
370
|
+
if (!globalAlertManager) {
|
|
371
|
+
globalAlertManager = new AlertManager();
|
|
372
|
+
}
|
|
373
|
+
return globalAlertManager;
|
|
374
|
+
}
|
|
375
|
+
export function resetAlertManager() {
|
|
376
|
+
if (globalAlertManager) {
|
|
377
|
+
globalAlertManager.stop();
|
|
378
|
+
}
|
|
379
|
+
globalAlertManager = null;
|
|
380
|
+
}
|
|
@@ -1,18 +1,19 @@
|
|
|
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
|
/**
|
|
6
|
-
* CacheService - Thread-safe TTL-based caching with
|
|
6
|
+
* CacheService - Thread-safe TTL-based caching with stale-while-revalidate
|
|
7
7
|
*/
|
|
8
8
|
export declare class CacheService {
|
|
9
9
|
private cache;
|
|
10
10
|
private cachePath;
|
|
11
11
|
private maxAge;
|
|
12
|
+
private staleAge;
|
|
12
13
|
private maxSize;
|
|
13
14
|
private savePending;
|
|
14
15
|
private saveTimer;
|
|
15
|
-
constructor(cachePath?: string, maxAge?: number, maxSize?: number);
|
|
16
|
+
constructor(cachePath?: string, maxAge?: number, maxSize?: number, staleAge?: number);
|
|
16
17
|
/**
|
|
17
18
|
* Generate a cache key from components
|
|
18
19
|
*/
|
|
@@ -24,7 +25,16 @@ export declare class CacheService {
|
|
|
24
25
|
/**
|
|
25
26
|
* Set a value in cache
|
|
26
27
|
*/
|
|
27
|
-
set<T>(key: string, data: T, ttl?: number): void;
|
|
28
|
+
set<T>(key: string, data: T, ttl?: number, staleTtl?: number): void;
|
|
29
|
+
/**
|
|
30
|
+
* Get or set with stale-while-revalidate pattern
|
|
31
|
+
* Returns cached value immediately, refreshes in background if stale
|
|
32
|
+
*/
|
|
33
|
+
getOrSetWithStaleRevalidate<T>(key: string, factory: () => T | Promise<T>, ttl?: number, staleTtl?: number): Promise<T>;
|
|
34
|
+
/**
|
|
35
|
+
* Refresh data in background
|
|
36
|
+
*/
|
|
37
|
+
private refreshInBackground;
|
|
28
38
|
/**
|
|
29
39
|
* Check if a key exists and is not expired
|
|
30
40
|
*/
|
|
@@ -49,6 +59,7 @@ export declare class CacheService {
|
|
|
49
59
|
hits: number;
|
|
50
60
|
age: number;
|
|
51
61
|
ttl: number;
|
|
62
|
+
isStale: boolean;
|
|
52
63
|
}>;
|
|
53
64
|
};
|
|
54
65
|
/**
|
|
@@ -56,7 +67,7 @@ export declare class CacheService {
|
|
|
56
67
|
*/
|
|
57
68
|
cleanup(): number;
|
|
58
69
|
/**
|
|
59
|
-
* Evict the oldest entry (LRU)
|
|
70
|
+
* Evict the oldest entry (LRU with hit weighting)
|
|
60
71
|
*/
|
|
61
72
|
private evictOldest;
|
|
62
73
|
/**
|
|
@@ -78,7 +89,7 @@ export declare class CacheService {
|
|
|
78
89
|
/**
|
|
79
90
|
* Get or set pattern - returns cached value or computes and caches it
|
|
80
91
|
*/
|
|
81
|
-
getOrSet<T>(key: string, factory: () => T | Promise<T>, ttl?: number): Promise<T>;
|
|
92
|
+
getOrSet<T>(key: string, factory: () => T | Promise<T>, ttl?: number, staleTtl?: number): Promise<T>;
|
|
82
93
|
/**
|
|
83
94
|
* Get multiple keys at once
|
|
84
95
|
*/
|
|
@@ -86,7 +97,7 @@ export declare class CacheService {
|
|
|
86
97
|
/**
|
|
87
98
|
* Set multiple keys at once
|
|
88
99
|
*/
|
|
89
|
-
setMultiple<T>(entries: Map<string, T>, ttl?: number): void;
|
|
100
|
+
setMultiple<T>(entries: Map<string, T>, ttl?: number, staleTtl?: number): void;
|
|
90
101
|
/**
|
|
91
102
|
* Invalidate cache by pattern
|
|
92
103
|
*/
|