dubs-server 1.0.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/.claude/settings.local.json +280 -0
- package/CLAUDE.md +46 -0
- package/CONNECT4_PRODUCTION_DEPLOY.md +155 -0
- package/CURRENT_SESSION.md +171 -0
- package/CURRENT_SESSION_DRAW.md +516 -0
- package/MARCH_MADNESS_SURVIVOR.md +254 -0
- package/PANDA.md +166 -0
- package/Procfile +4 -0
- package/README.md +476 -0
- package/controllers/livescoresController.js +376 -0
- package/controllers/pickemController.js +554 -0
- package/controllers/survivorAdminController.js +887 -0
- package/controllers/survivorController.js +623 -0
- package/cron/oracleMonitor.js +77 -0
- package/cron/pickemOracleMonitor.js +73 -0
- package/data/jackpot-history.json +952 -0
- package/data/ncaaTeams.js +406 -0
- package/documentation/API_SECURITY_GUIDE.md +327 -0
- package/documentation/ARCADE_API.md +593 -0
- package/documentation/ARCADE_IMPLEMENTATION_SUMMARY.md +399 -0
- package/documentation/ARCADE_QUICKSTART.md +242 -0
- package/documentation/AUTOMATIC_MODE_ORACLE.md +321 -0
- package/documentation/BUG_FIX_COHORT_DATE_DISPLAY.md +171 -0
- package/documentation/CLAIM_MIGRATION_INSTRUCTIONS.md +52 -0
- package/documentation/CLAIM_STATUS_FIX.md +67 -0
- package/documentation/CLI_TOOL_GUIDE.md +372 -0
- package/documentation/COHORT_RETENTION_ANALYSIS.md +295 -0
- package/documentation/COHORT_RETENTION_IMPLEMENTATION_COMPLETE.md +461 -0
- package/documentation/COHORT_RETENTION_SUMMARY.md +204 -0
- package/documentation/COMPLETE_PROJECT_SUMMARY.md +490 -0
- package/documentation/DATABASE_QUERIES.md +269 -0
- package/documentation/DATABASE_RETENTION_POLICY.md +390 -0
- package/documentation/DATABASE_SETUP_GUIDE.md +361 -0
- package/documentation/DATABASE_SETUP_SUMMARY.md +247 -0
- package/documentation/DEMO_API_CURL_COMMANDS.md +656 -0
- package/documentation/DEPLOYMENT_SUMMARY.txt +100 -0
- package/documentation/DUPLICATE_NOTIFICATIONS_FIXED.md +201 -0
- package/documentation/EXCHANGE_RATES_INTEGRATION.md +371 -0
- package/documentation/FINAL_API_PROTECTION_TABLE.md +175 -0
- package/documentation/GAME_START_NOTIFICATIONS_DEPLOYMENT.md +256 -0
- package/documentation/GAME_START_NOTIFICATIONS_INTEGRATION.md +275 -0
- package/documentation/HEROKU_DEPLOYMENT.md +134 -0
- package/documentation/HEROKU_SCHEDULER_SETUP.md +271 -0
- package/documentation/JACKPOT_API.md +521 -0
- package/documentation/JACKPOT_DEPLOYMENT_GUIDE.md +362 -0
- package/documentation/JWT_IMPLEMENTATION_SUMMARY.md +373 -0
- package/documentation/JWT_QUICK_SETUP.md +268 -0
- package/documentation/JWT_TESTING_GUIDE.md +404 -0
- package/documentation/KEEPER_RECOVERY_GUIDE.md +381 -0
- package/documentation/KEEPER_SETUP.md +206 -0
- package/documentation/KEEPER_STATE_MACHINE.md +423 -0
- package/documentation/LATEST_PRODUCTION_SETUP.md +387 -0
- package/documentation/LOCAL_VOTING_TEST.md +279 -0
- package/documentation/ORACLE_FIXES_SUMMARY.md +188 -0
- package/documentation/ORACLE_POSTGRESQL_UPDATE.md +202 -0
- package/documentation/PAYMENT_DEPLOYMENT.md +209 -0
- package/documentation/PNL_TRACKING_SETUP.md +189 -0
- package/documentation/PREVENTING_LOCKUP_ERRORS.md +472 -0
- package/documentation/PRODUCTION_READY_SUMMARY.md +227 -0
- package/documentation/PUBLIC_VS_PRIVATE_ENDPOINTS.md +278 -0
- package/documentation/QUICK_AUTH_SETUP.md +99 -0
- package/documentation/QUICK_DEPLOY.md +224 -0
- package/documentation/QUICK_FIX.md +114 -0
- package/documentation/QUICK_START.md +152 -0
- package/documentation/REFEREE_MODE_GUIDE.md +392 -0
- package/documentation/RETENTION_CORE_ACTION_UPDATE.md +313 -0
- package/documentation/RETENTION_UPDATE_SUMMARY.md +108 -0
- package/documentation/RUN_MIGRATION_NOW.md +39 -0
- package/documentation/SCRIPTS_UPDATE_SUMMARY.md +251 -0
- package/documentation/SETUP_GUIDE.md +184 -0
- package/documentation/STATE_MACHINE_IMPLEMENTATION.md +250 -0
- package/documentation/TELEGRAM_NOTIFICATIONS_DIAGNOSIS.md +361 -0
- package/documentation/UNIFIED_ARCHITECTURE.md +231 -0
- package/documentation/VOTING_DEPLOYMENT_SUMMARY.md +392 -0
- package/documentation/WEBSOCKET_ARCHITECTURE.md +881 -0
- package/documentation/WHAT_WE_BUILT_TODAY.md +369 -0
- package/documentation/latest/LATEST_PRODUCTION_SETUP.md +865 -0
- package/ecosystem.config.js +65 -0
- package/env.template +125 -0
- package/middleware/apiKeyAuth.js +136 -0
- package/middleware/authenticate.js +214 -0
- package/middleware/developerUserAuth.js +76 -0
- package/middleware/socketAuth.js +69 -0
- package/package.json +49 -0
- package/postman/Dubs-API-v1-With-Voting.postman_collection.json +555 -0
- package/postman/Dubs-API-v1.postman_collection.json +205 -0
- package/postman/Dubs_Developer_API.postman_collection.json +662 -0
- package/postman/QUICKSTART.md +118 -0
- package/postman/QUICK_REFERENCE.md +246 -0
- package/postman/README.md +71 -0
- package/postman/VOTING_API_GUIDE.md +426 -0
- package/refactor/Animations.md +148 -0
- package/refactor/Chat.md +252 -0
- package/routes/actionsRoutes.js +699 -0
- package/routes/adminRoutes.js +370 -0
- package/routes/analyticsRoutes.js +1262 -0
- package/routes/arcadeRoutes.js +557 -0
- package/routes/authRoutes.js +2310 -0
- package/routes/avatarRoutes.js +85 -0
- package/routes/botRoutes.js +211 -0
- package/routes/chatRoutes.js +377 -0
- package/routes/cryptoPriceRoutes.js +105 -0
- package/routes/developerRoutes.js +4201 -0
- package/routes/deviceRoutes.js +214 -0
- package/routes/dmRoutes.js +167 -0
- package/routes/esportsRoutes.js +806 -0
- package/routes/exchangeRateRoutes.js +233 -0
- package/routes/gamesRoutes.js +3028 -0
- package/routes/jackpotRoutes.js +754 -0
- package/routes/keeperMonitoringRoutes.js +156 -0
- package/routes/keeperWebhookRoutes.js +466 -0
- package/routes/livescoresRoutes.js +31 -0
- package/routes/pickemAdminRoutes.js +199 -0
- package/routes/pickemRoutes.js +231 -0
- package/routes/playerStatsRoutes.js +147 -0
- package/routes/portfolioRoutes.js +217 -0
- package/routes/promoRoutes.js +418 -0
- package/routes/referralEarningsRoutes.js +392 -0
- package/routes/socialRoutes.js +459 -0
- package/routes/sportsRoutes.js +1271 -0
- package/routes/survivorAdminRoutes.js +345 -0
- package/routes/survivorRoutes.js +756 -0
- package/routes/uploadRoutes.js +256 -0
- package/routes/userProfileRoutes.js +244 -0
- package/routes/whatsNewRoutes.js +331 -0
- package/scripts/.claude/settings.local.json +15 -0
- package/scripts/README.md +170 -0
- package/scripts/RESTART_EVERYTHING.sh +104 -0
- package/scripts/add-claim-columns.sql +48 -0
- package/scripts/add-crypto-prices-cache.sql +27 -0
- package/scripts/add-exchange-rates-cache.sql +40 -0
- package/scripts/add-game-invite-column.sql +23 -0
- package/scripts/add-game-invite-notification.sql +33 -0
- package/scripts/add-game-invite-telegram-pref.sql +16 -0
- package/scripts/add-game-joined-notification.sql +16 -0
- package/scripts/add-game-joined-pref.js +40 -0
- package/scripts/add-game-joined-preference.sql +6 -0
- package/scripts/add-game-start-notifications.sql +41 -0
- package/scripts/add-notification-flags-to-games.sql +55 -0
- package/scripts/add-pending-game-dismissals.sql +19 -0
- package/scripts/add-preferred-currency.sql +34 -0
- package/scripts/add-winner-columns.js +61 -0
- package/scripts/add_mention_system.sql +53 -0
- package/scripts/add_payment_system.sql +96 -0
- package/scripts/add_sports_event_id_column.sql +22 -0
- package/scripts/analyze-cohort-data-heroku.js +276 -0
- package/scripts/analyze-cohort-data.js +295 -0
- package/scripts/analyze-prod-cohorts.sh +10 -0
- package/scripts/backfill-matchup-images.js +245 -0
- package/scripts/backfill-missing-signatures.js +175 -0
- package/scripts/backfill-referral-earnings.js +202 -0
- package/scripts/check-chat-schema.js +130 -0
- package/scripts/check-db.sh +14 -0
- package/scripts/check_oracle_in_game.js +54 -0
- package/scripts/cleanup-database.js +193 -0
- package/scripts/clear-notification-cache.js +85 -0
- package/scripts/convert-mnemonic.js +50 -0
- package/scripts/create-users-table.sql +44 -0
- package/scripts/debug-cohort-counts.js +248 -0
- package/scripts/debug-winner-calc.js +84 -0
- package/scripts/deploy-payment-system.sh +118 -0
- package/scripts/deploy-to-heroku.sh +63 -0
- package/scripts/diagnose-locked-round.js +143 -0
- package/scripts/dubs-cli.js +720 -0
- package/scripts/dump-account.js +65 -0
- package/scripts/find-vrf-offset.js +48 -0
- package/scripts/fix-chat-notifications-constraint.sql +122 -0
- package/scripts/fix-claim-columns.js +124 -0
- package/scripts/fix-constraint-now.js +44 -0
- package/scripts/fix-lock-timestamps.js +96 -0
- package/scripts/fix-locked-round.sh +126 -0
- package/scripts/fix-missing-badges.sql +91 -0
- package/scripts/fix-payment-notifications.sql +41 -0
- package/scripts/force-new-round.js +55 -0
- package/scripts/force-resolve-and-claim.js +278 -0
- package/scripts/important/README.md +115 -0
- package/scripts/important/authority-force-lock.js +197 -0
- package/scripts/important/authority-resolve-game.js +267 -0
- package/scripts/important/check-game-status.js +373 -0
- package/scripts/important/list-pending-games-by-version.js +270 -0
- package/scripts/important/reconcile-v1-v2-payouts.js +270 -0
- package/scripts/initialize-jackpot.js +111 -0
- package/scripts/jackpot/.claude/settings.local.json +10 -0
- package/scripts/jackpot/force-reset.js +84 -0
- package/scripts/jackpot/initialize-mainnet.js +100 -0
- package/scripts/jackpot/keeper.js +742 -0
- package/scripts/jackpot/status.js +107 -0
- package/scripts/jackpot/update-round-duration.js +143 -0
- package/scripts/keeper-bot.js +112 -0
- package/scripts/list-pending-games.js +131 -0
- package/scripts/migrate-chat-v2.js +127 -0
- package/scripts/migrate-chat-winners.js +84 -0
- package/scripts/migrate-chat.sh +17 -0
- package/scripts/migrate-game-invite.js +83 -0
- package/scripts/migrate-heroku-game-notifications.sh +159 -0
- package/scripts/migrations/001_analytics_tables.sql +422 -0
- package/scripts/migrations/002_add_matchup_image_url.sql +14 -0
- package/scripts/migrations/003_referral_earnings.sql +208 -0
- package/scripts/migrations/004_add_whats_new_notification_type.sql +62 -0
- package/scripts/migrations/005_add_connect4_your_turn_notification.sql +61 -0
- package/scripts/migrations/005_push_notifications.sql +55 -0
- package/scripts/migrations/006_add_draw_team_players.sql +28 -0
- package/scripts/migrations/006_add_game_cancelled_notification.sql +62 -0
- package/scripts/migrations/007_add_gif_url.sql +8 -0
- package/scripts/migrations/008_add_connect4_columns.sql +139 -0
- package/scripts/migrations/008_add_pool_tracking.sql +22 -0
- package/scripts/migrations/009_create_survivor_pool_tables.sql +174 -0
- package/scripts/migrations/010_add_survivor_pool_outcome.sql +28 -0
- package/scripts/migrations/011_create_developer_tables.sql +67 -0
- package/scripts/migrations/011_fix_keeper_tables.sql +85 -0
- package/scripts/migrations/012_create_developer_webhooks.sql +31 -0
- package/scripts/migrations/013_add_network_mode.sql +18 -0
- package/scripts/migrations/014_create_developer_app_users.sql +19 -0
- package/scripts/migrations/015_add_ui_config.sql +4 -0
- package/scripts/migrations/016_add_resolution_secret.sql +4 -0
- package/scripts/migrations/017_add_external_game_id.sql +3 -0
- package/scripts/migrations/018_create_pickem_tables.sql +115 -0
- package/scripts/migrations/019_expo_push_tokens.sql +19 -0
- package/scripts/migrations/create_whats_new_tables.sql +88 -0
- package/scripts/migrations/drop_live_games_tables.sql +34 -0
- package/scripts/open-jackpot-round.js +85 -0
- package/scripts/purge-all-data.sh +329 -0
- package/scripts/purge-all-data.sql +142 -0
- package/scripts/purge-heroku-data.sh +149 -0
- package/scripts/purge-heroku-data.sql +62 -0
- package/scripts/rebuild-heroku-database.sh +113 -0
- package/scripts/recover-funds.js +357 -0
- package/scripts/regenerate-epl-images.js +278 -0
- package/scripts/resize-s3-matchup-images.js +374 -0
- package/scripts/resolve-direct.js +88 -0
- package/scripts/resolve-mock-game.js +124 -0
- package/scripts/resolve-pickem-game.js +55 -0
- package/scripts/resolve-round-manual.js +83 -0
- package/scripts/resolve-stuck-game.js +382 -0
- package/scripts/resolve-stuck-round.js +42 -0
- package/scripts/run-connect4-migration.sh +16 -0
- package/scripts/run-mention-migration.sh +32 -0
- package/scripts/run-payment-migration.sh +51 -0
- package/scripts/run-preferred-currency-migration.sh +31 -0
- package/scripts/run-referral-earnings-migration.sh +32 -0
- package/scripts/run-survivor-outcome-migration.sh +16 -0
- package/scripts/seed-test-users.js +346 -0
- package/scripts/setup-auth-tables.js +78 -0
- package/scripts/setup-complete-database.sql +992 -0
- package/scripts/setup-database-fresh.sh +359 -0
- package/scripts/setup-heroku-keeper.sh +48 -0
- package/scripts/setup-keeper-database.js +83 -0
- package/scripts/setup-keeper-state-db.sql +110 -0
- package/scripts/setup-oracle.sh +39 -0
- package/scripts/setup-pnl-tracking.js +111 -0
- package/scripts/start-devnet.sh +14 -0
- package/scripts/test-arcade-devnet.sh +160 -0
- package/scripts/test-arcade-match.sh +109 -0
- package/scripts/test-automatic-mode.sh +239 -0
- package/scripts/test-connect4-cancel-claim.js +370 -0
- package/scripts/test-connect4-e2e.js +369 -0
- package/scripts/test-connect4-resolve.js +369 -0
- package/scripts/test-game-state-endpoint.js +136 -0
- package/scripts/test-invite-notification.js +86 -0
- package/scripts/test-jackpot-api.sh +71 -0
- package/scripts/test-poll-confirmation.js +267 -0
- package/scripts/test-resolve-game.js +271 -0
- package/scripts/test-resolve-signature.js +223 -0
- package/scripts/test-signature-preservation.js +124 -0
- package/scripts/test-state-machine.js +291 -0
- package/scripts/test-webhook-receiver.js +60 -0
- package/scripts/update-notification-constraint.js +52 -0
- package/scripts/verify-account-layout.js +145 -0
- package/scripts/verify-winner-algorithm.js +278 -0
- package/server.js +5259 -0
- package/services/arcadeMatchService.js +763 -0
- package/services/automaticGameOracle.js +1596 -0
- package/services/chatService.js +1612 -0
- package/services/connect4GameService.js +1049 -0
- package/services/connect4NotificationService.js +374 -0
- package/services/cryptoPriceService.js +223 -0
- package/services/customGameResolver.js +260 -0
- package/services/db.js +79 -0
- package/services/directMessageService.js +389 -0
- package/services/discordNotifications.js +160 -0
- package/services/exchangeRateService.js +289 -0
- package/services/expoPushService.js +314 -0
- package/services/gamesCacheService.js +539 -0
- package/services/jackpotHistory.js +331 -0
- package/services/jackpotService.js +856 -0
- package/services/keeperStateService.js +355 -0
- package/services/matchupImageService.js +591 -0
- package/services/notificationCacheService.js +407 -0
- package/services/pickemOracle.js +440 -0
- package/services/playerStatsService.js +389 -0
- package/services/portfolioService.js +555 -0
- package/services/promoService.js +757 -0
- package/services/promoTreasuryService.js +239 -0
- package/services/pushNotifications.js +353 -0
- package/services/redisService.js +422 -0
- package/services/referralEarningsService.js +728 -0
- package/services/s3Service.js +396 -0
- package/services/socialService.js +1202 -0
- package/services/survivorOracle.js +469 -0
- package/services/survivorSimulator.js +475 -0
- package/services/telegramNotifications.js +461 -0
- package/services/userProfileStatsService.js +1185 -0
- package/services/whatsNewService.js +388 -0
- package/utils/urlHelper.js +95 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis Service - High-performance caching layer
|
|
3
|
+
*
|
|
4
|
+
* Provides connection management with graceful fallback when Redis is unavailable.
|
|
5
|
+
* Used primarily for notification caching but designed for general-purpose caching.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const Redis = require('ioredis');
|
|
9
|
+
|
|
10
|
+
class RedisService {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.client = null;
|
|
13
|
+
this.isConnected = false;
|
|
14
|
+
this.connectionAttempts = 0;
|
|
15
|
+
this.maxRetries = 3;
|
|
16
|
+
this.retryDelay = 1000;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Initialize Redis connection
|
|
21
|
+
* Gracefully handles missing REDIS_URL - will operate in fallback mode
|
|
22
|
+
*/
|
|
23
|
+
async connect() {
|
|
24
|
+
const redisUrl = process.env.REDIS_URL;
|
|
25
|
+
|
|
26
|
+
if (!redisUrl) {
|
|
27
|
+
console.log('⚠️ [Redis] REDIS_URL not configured - running in PostgreSQL-only mode');
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
this.client = new Redis(redisUrl, {
|
|
33
|
+
maxRetriesPerRequest: 3,
|
|
34
|
+
retryDelayOnFailover: 100,
|
|
35
|
+
enableReadyCheck: true,
|
|
36
|
+
lazyConnect: false,
|
|
37
|
+
// Connection pool settings
|
|
38
|
+
family: 4, // IPv4
|
|
39
|
+
connectTimeout: 10000,
|
|
40
|
+
// Heroku Redis requires TLS
|
|
41
|
+
tls: redisUrl.startsWith('rediss://') ? { rejectUnauthorized: false } : undefined,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Event handlers
|
|
45
|
+
this.client.on('connect', () => {
|
|
46
|
+
console.log('✅ [Redis] Connected successfully');
|
|
47
|
+
this.isConnected = true;
|
|
48
|
+
this.connectionAttempts = 0;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
this.client.on('error', (err) => {
|
|
52
|
+
console.error('❌ [Redis] Connection error:', err.message);
|
|
53
|
+
this.isConnected = false;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
this.client.on('close', () => {
|
|
57
|
+
console.log('🔌 [Redis] Connection closed');
|
|
58
|
+
this.isConnected = false;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
this.client.on('reconnecting', () => {
|
|
62
|
+
this.connectionAttempts++;
|
|
63
|
+
console.log(`🔄 [Redis] Reconnecting (attempt ${this.connectionAttempts})...`);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Test connection
|
|
67
|
+
await this.client.ping();
|
|
68
|
+
this.isConnected = true;
|
|
69
|
+
return true;
|
|
70
|
+
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error('❌ [Redis] Failed to connect:', error.message);
|
|
73
|
+
this.isConnected = false;
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if Redis is available
|
|
80
|
+
*/
|
|
81
|
+
isAvailable() {
|
|
82
|
+
return this.isConnected && this.client !== null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Graceful disconnect
|
|
87
|
+
*/
|
|
88
|
+
async disconnect() {
|
|
89
|
+
if (this.client) {
|
|
90
|
+
await this.client.quit();
|
|
91
|
+
this.client = null;
|
|
92
|
+
this.isConnected = false;
|
|
93
|
+
console.log('🔌 [Redis] Disconnected gracefully');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ============================================
|
|
98
|
+
// SORTED SET OPERATIONS (for paginated lists)
|
|
99
|
+
// ============================================
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Add item to sorted set with score (timestamp)
|
|
103
|
+
* @param {string} key - Sorted set key
|
|
104
|
+
* @param {number} score - Score (typically timestamp)
|
|
105
|
+
* @param {string} member - Member value (typically ID)
|
|
106
|
+
*/
|
|
107
|
+
async zadd(key, score, member) {
|
|
108
|
+
if (!this.isAvailable()) return false;
|
|
109
|
+
try {
|
|
110
|
+
await this.client.zadd(key, score, member);
|
|
111
|
+
return true;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error('[Redis] zadd error:', error.message);
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get range from sorted set (high to low score)
|
|
120
|
+
* Perfect for timestamp-based pagination
|
|
121
|
+
* @param {string} key - Sorted set key
|
|
122
|
+
* @param {number} start - Start index
|
|
123
|
+
* @param {number} stop - Stop index
|
|
124
|
+
* @param {boolean} withScores - Include scores in result
|
|
125
|
+
*/
|
|
126
|
+
async zrevrange(key, start, stop, withScores = false) {
|
|
127
|
+
if (!this.isAvailable()) return null;
|
|
128
|
+
try {
|
|
129
|
+
if (withScores) {
|
|
130
|
+
return await this.client.zrevrange(key, start, stop, 'WITHSCORES');
|
|
131
|
+
}
|
|
132
|
+
return await this.client.zrevrange(key, start, stop);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error('[Redis] zrevrange error:', error.message);
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get range by score (for cursor-based pagination)
|
|
141
|
+
* @param {string} key - Sorted set key
|
|
142
|
+
* @param {number} max - Max score (exclusive with '(' prefix)
|
|
143
|
+
* @param {number} min - Min score
|
|
144
|
+
* @param {number} offset - Offset for pagination
|
|
145
|
+
* @param {number} count - Number of items
|
|
146
|
+
*/
|
|
147
|
+
async zrevrangebyscore(key, max, min, offset = 0, count = 10) {
|
|
148
|
+
if (!this.isAvailable()) return null;
|
|
149
|
+
try {
|
|
150
|
+
return await this.client.zrevrangebyscore(key, max, min, 'LIMIT', offset, count);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error('[Redis] zrevrangebyscore error:', error.message);
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get score for a member
|
|
159
|
+
*/
|
|
160
|
+
async zscore(key, member) {
|
|
161
|
+
if (!this.isAvailable()) return null;
|
|
162
|
+
try {
|
|
163
|
+
const score = await this.client.zscore(key, member);
|
|
164
|
+
return score ? parseFloat(score) : null;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error('[Redis] zscore error:', error.message);
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get count of items in sorted set
|
|
173
|
+
*/
|
|
174
|
+
async zcard(key) {
|
|
175
|
+
if (!this.isAvailable()) return null;
|
|
176
|
+
try {
|
|
177
|
+
return await this.client.zcard(key);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error('[Redis] zcard error:', error.message);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Remove member from sorted set
|
|
186
|
+
*/
|
|
187
|
+
async zrem(key, member) {
|
|
188
|
+
if (!this.isAvailable()) return false;
|
|
189
|
+
try {
|
|
190
|
+
await this.client.zrem(key, member);
|
|
191
|
+
return true;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error('[Redis] zrem error:', error.message);
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ============================================
|
|
199
|
+
// HASH OPERATIONS (for structured data)
|
|
200
|
+
// ============================================
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Set hash field(s)
|
|
204
|
+
* @param {string} key - Hash key
|
|
205
|
+
* @param {Object} data - Object with field:value pairs
|
|
206
|
+
*/
|
|
207
|
+
async hset(key, data) {
|
|
208
|
+
if (!this.isAvailable()) return false;
|
|
209
|
+
try {
|
|
210
|
+
await this.client.hset(key, data);
|
|
211
|
+
return true;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.error('[Redis] hset error:', error.message);
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get all fields from hash
|
|
220
|
+
*/
|
|
221
|
+
async hgetall(key) {
|
|
222
|
+
if (!this.isAvailable()) return null;
|
|
223
|
+
try {
|
|
224
|
+
const result = await this.client.hgetall(key);
|
|
225
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.error('[Redis] hgetall error:', error.message);
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get multiple hashes at once (for batch lookups)
|
|
234
|
+
* @param {string[]} keys - Array of hash keys
|
|
235
|
+
*/
|
|
236
|
+
async hmgetall(keys) {
|
|
237
|
+
if (!this.isAvailable() || keys.length === 0) return null;
|
|
238
|
+
try {
|
|
239
|
+
const pipeline = this.client.pipeline();
|
|
240
|
+
keys.forEach(key => pipeline.hgetall(key));
|
|
241
|
+
const results = await pipeline.exec();
|
|
242
|
+
return results.map(([err, data]) => {
|
|
243
|
+
if (err) return null;
|
|
244
|
+
return Object.keys(data).length > 0 ? data : null;
|
|
245
|
+
});
|
|
246
|
+
} catch (error) {
|
|
247
|
+
console.error('[Redis] hmgetall error:', error.message);
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Delete hash
|
|
254
|
+
*/
|
|
255
|
+
async hdel(key) {
|
|
256
|
+
if (!this.isAvailable()) return false;
|
|
257
|
+
try {
|
|
258
|
+
await this.client.del(key);
|
|
259
|
+
return true;
|
|
260
|
+
} catch (error) {
|
|
261
|
+
console.error('[Redis] hdel error:', error.message);
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ============================================
|
|
267
|
+
// COUNTER OPERATIONS
|
|
268
|
+
// ============================================
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Increment counter
|
|
272
|
+
*/
|
|
273
|
+
async incr(key) {
|
|
274
|
+
if (!this.isAvailable()) return null;
|
|
275
|
+
try {
|
|
276
|
+
return await this.client.incr(key);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
console.error('[Redis] incr error:', error.message);
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Decrement counter (min 0)
|
|
285
|
+
*/
|
|
286
|
+
async decr(key) {
|
|
287
|
+
if (!this.isAvailable()) return null;
|
|
288
|
+
try {
|
|
289
|
+
const result = await this.client.decr(key);
|
|
290
|
+
// Ensure we don't go below 0
|
|
291
|
+
if (result < 0) {
|
|
292
|
+
await this.client.set(key, 0);
|
|
293
|
+
return 0;
|
|
294
|
+
}
|
|
295
|
+
return result;
|
|
296
|
+
} catch (error) {
|
|
297
|
+
console.error('[Redis] decr error:', error.message);
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Set counter to specific value
|
|
304
|
+
*/
|
|
305
|
+
async set(key, value, ttlSeconds = null) {
|
|
306
|
+
if (!this.isAvailable()) return false;
|
|
307
|
+
try {
|
|
308
|
+
if (ttlSeconds) {
|
|
309
|
+
await this.client.set(key, value, 'EX', ttlSeconds);
|
|
310
|
+
} else {
|
|
311
|
+
await this.client.set(key, value);
|
|
312
|
+
}
|
|
313
|
+
return true;
|
|
314
|
+
} catch (error) {
|
|
315
|
+
console.error('[Redis] set error:', error.message);
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Get value
|
|
322
|
+
*/
|
|
323
|
+
async get(key) {
|
|
324
|
+
if (!this.isAvailable()) return null;
|
|
325
|
+
try {
|
|
326
|
+
return await this.client.get(key);
|
|
327
|
+
} catch (error) {
|
|
328
|
+
console.error('[Redis] get error:', error.message);
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ============================================
|
|
334
|
+
// TTL & CLEANUP
|
|
335
|
+
// ============================================
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Set expiry on key
|
|
339
|
+
*/
|
|
340
|
+
async expire(key, seconds) {
|
|
341
|
+
if (!this.isAvailable()) return false;
|
|
342
|
+
try {
|
|
343
|
+
await this.client.expire(key, seconds);
|
|
344
|
+
return true;
|
|
345
|
+
} catch (error) {
|
|
346
|
+
console.error('[Redis] expire error:', error.message);
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Delete key(s)
|
|
353
|
+
*/
|
|
354
|
+
async del(...keys) {
|
|
355
|
+
if (!this.isAvailable()) return false;
|
|
356
|
+
try {
|
|
357
|
+
await this.client.del(...keys);
|
|
358
|
+
return true;
|
|
359
|
+
} catch (error) {
|
|
360
|
+
console.error('[Redis] del error:', error.message);
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Check if key exists
|
|
367
|
+
*/
|
|
368
|
+
async exists(key) {
|
|
369
|
+
if (!this.isAvailable()) return false;
|
|
370
|
+
try {
|
|
371
|
+
return (await this.client.exists(key)) === 1;
|
|
372
|
+
} catch (error) {
|
|
373
|
+
console.error('[Redis] exists error:', error.message);
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ============================================
|
|
379
|
+
// SCAN OPERATIONS (for pattern-based key lookup)
|
|
380
|
+
// ============================================
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Scan for keys matching a pattern using SCAN (non-blocking, production-safe).
|
|
384
|
+
* @param {string} pattern - Glob pattern (e.g. "user_game:*")
|
|
385
|
+
* @returns {string[]} - All matching keys
|
|
386
|
+
*/
|
|
387
|
+
async scanKeys(pattern) {
|
|
388
|
+
if (!this.isAvailable()) return [];
|
|
389
|
+
try {
|
|
390
|
+
const keys = [];
|
|
391
|
+
let cursor = '0';
|
|
392
|
+
do {
|
|
393
|
+
const [nextCursor, batch] = await this.client.scan(cursor, 'MATCH', pattern, 'COUNT', 200);
|
|
394
|
+
cursor = nextCursor;
|
|
395
|
+
keys.push(...batch);
|
|
396
|
+
} while (cursor !== '0');
|
|
397
|
+
return keys;
|
|
398
|
+
} catch (error) {
|
|
399
|
+
console.error('[Redis] scanKeys error:', error.message);
|
|
400
|
+
return [];
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ============================================
|
|
405
|
+
// PIPELINE (for batch operations)
|
|
406
|
+
// ============================================
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Create pipeline for batch operations
|
|
410
|
+
*/
|
|
411
|
+
pipeline() {
|
|
412
|
+
if (!this.isAvailable()) return null;
|
|
413
|
+
return this.client.pipeline();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Singleton instance
|
|
418
|
+
const redisService = new RedisService();
|
|
419
|
+
|
|
420
|
+
module.exports = redisService;
|
|
421
|
+
|
|
422
|
+
|