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.
Files changed (304) hide show
  1. package/.claude/settings.local.json +280 -0
  2. package/CLAUDE.md +46 -0
  3. package/CONNECT4_PRODUCTION_DEPLOY.md +155 -0
  4. package/CURRENT_SESSION.md +171 -0
  5. package/CURRENT_SESSION_DRAW.md +516 -0
  6. package/MARCH_MADNESS_SURVIVOR.md +254 -0
  7. package/PANDA.md +166 -0
  8. package/Procfile +4 -0
  9. package/README.md +476 -0
  10. package/controllers/livescoresController.js +376 -0
  11. package/controllers/pickemController.js +554 -0
  12. package/controllers/survivorAdminController.js +887 -0
  13. package/controllers/survivorController.js +623 -0
  14. package/cron/oracleMonitor.js +77 -0
  15. package/cron/pickemOracleMonitor.js +73 -0
  16. package/data/jackpot-history.json +952 -0
  17. package/data/ncaaTeams.js +406 -0
  18. package/documentation/API_SECURITY_GUIDE.md +327 -0
  19. package/documentation/ARCADE_API.md +593 -0
  20. package/documentation/ARCADE_IMPLEMENTATION_SUMMARY.md +399 -0
  21. package/documentation/ARCADE_QUICKSTART.md +242 -0
  22. package/documentation/AUTOMATIC_MODE_ORACLE.md +321 -0
  23. package/documentation/BUG_FIX_COHORT_DATE_DISPLAY.md +171 -0
  24. package/documentation/CLAIM_MIGRATION_INSTRUCTIONS.md +52 -0
  25. package/documentation/CLAIM_STATUS_FIX.md +67 -0
  26. package/documentation/CLI_TOOL_GUIDE.md +372 -0
  27. package/documentation/COHORT_RETENTION_ANALYSIS.md +295 -0
  28. package/documentation/COHORT_RETENTION_IMPLEMENTATION_COMPLETE.md +461 -0
  29. package/documentation/COHORT_RETENTION_SUMMARY.md +204 -0
  30. package/documentation/COMPLETE_PROJECT_SUMMARY.md +490 -0
  31. package/documentation/DATABASE_QUERIES.md +269 -0
  32. package/documentation/DATABASE_RETENTION_POLICY.md +390 -0
  33. package/documentation/DATABASE_SETUP_GUIDE.md +361 -0
  34. package/documentation/DATABASE_SETUP_SUMMARY.md +247 -0
  35. package/documentation/DEMO_API_CURL_COMMANDS.md +656 -0
  36. package/documentation/DEPLOYMENT_SUMMARY.txt +100 -0
  37. package/documentation/DUPLICATE_NOTIFICATIONS_FIXED.md +201 -0
  38. package/documentation/EXCHANGE_RATES_INTEGRATION.md +371 -0
  39. package/documentation/FINAL_API_PROTECTION_TABLE.md +175 -0
  40. package/documentation/GAME_START_NOTIFICATIONS_DEPLOYMENT.md +256 -0
  41. package/documentation/GAME_START_NOTIFICATIONS_INTEGRATION.md +275 -0
  42. package/documentation/HEROKU_DEPLOYMENT.md +134 -0
  43. package/documentation/HEROKU_SCHEDULER_SETUP.md +271 -0
  44. package/documentation/JACKPOT_API.md +521 -0
  45. package/documentation/JACKPOT_DEPLOYMENT_GUIDE.md +362 -0
  46. package/documentation/JWT_IMPLEMENTATION_SUMMARY.md +373 -0
  47. package/documentation/JWT_QUICK_SETUP.md +268 -0
  48. package/documentation/JWT_TESTING_GUIDE.md +404 -0
  49. package/documentation/KEEPER_RECOVERY_GUIDE.md +381 -0
  50. package/documentation/KEEPER_SETUP.md +206 -0
  51. package/documentation/KEEPER_STATE_MACHINE.md +423 -0
  52. package/documentation/LATEST_PRODUCTION_SETUP.md +387 -0
  53. package/documentation/LOCAL_VOTING_TEST.md +279 -0
  54. package/documentation/ORACLE_FIXES_SUMMARY.md +188 -0
  55. package/documentation/ORACLE_POSTGRESQL_UPDATE.md +202 -0
  56. package/documentation/PAYMENT_DEPLOYMENT.md +209 -0
  57. package/documentation/PNL_TRACKING_SETUP.md +189 -0
  58. package/documentation/PREVENTING_LOCKUP_ERRORS.md +472 -0
  59. package/documentation/PRODUCTION_READY_SUMMARY.md +227 -0
  60. package/documentation/PUBLIC_VS_PRIVATE_ENDPOINTS.md +278 -0
  61. package/documentation/QUICK_AUTH_SETUP.md +99 -0
  62. package/documentation/QUICK_DEPLOY.md +224 -0
  63. package/documentation/QUICK_FIX.md +114 -0
  64. package/documentation/QUICK_START.md +152 -0
  65. package/documentation/REFEREE_MODE_GUIDE.md +392 -0
  66. package/documentation/RETENTION_CORE_ACTION_UPDATE.md +313 -0
  67. package/documentation/RETENTION_UPDATE_SUMMARY.md +108 -0
  68. package/documentation/RUN_MIGRATION_NOW.md +39 -0
  69. package/documentation/SCRIPTS_UPDATE_SUMMARY.md +251 -0
  70. package/documentation/SETUP_GUIDE.md +184 -0
  71. package/documentation/STATE_MACHINE_IMPLEMENTATION.md +250 -0
  72. package/documentation/TELEGRAM_NOTIFICATIONS_DIAGNOSIS.md +361 -0
  73. package/documentation/UNIFIED_ARCHITECTURE.md +231 -0
  74. package/documentation/VOTING_DEPLOYMENT_SUMMARY.md +392 -0
  75. package/documentation/WEBSOCKET_ARCHITECTURE.md +881 -0
  76. package/documentation/WHAT_WE_BUILT_TODAY.md +369 -0
  77. package/documentation/latest/LATEST_PRODUCTION_SETUP.md +865 -0
  78. package/ecosystem.config.js +65 -0
  79. package/env.template +125 -0
  80. package/middleware/apiKeyAuth.js +136 -0
  81. package/middleware/authenticate.js +214 -0
  82. package/middleware/developerUserAuth.js +76 -0
  83. package/middleware/socketAuth.js +69 -0
  84. package/package.json +49 -0
  85. package/postman/Dubs-API-v1-With-Voting.postman_collection.json +555 -0
  86. package/postman/Dubs-API-v1.postman_collection.json +205 -0
  87. package/postman/Dubs_Developer_API.postman_collection.json +662 -0
  88. package/postman/QUICKSTART.md +118 -0
  89. package/postman/QUICK_REFERENCE.md +246 -0
  90. package/postman/README.md +71 -0
  91. package/postman/VOTING_API_GUIDE.md +426 -0
  92. package/refactor/Animations.md +148 -0
  93. package/refactor/Chat.md +252 -0
  94. package/routes/actionsRoutes.js +699 -0
  95. package/routes/adminRoutes.js +370 -0
  96. package/routes/analyticsRoutes.js +1262 -0
  97. package/routes/arcadeRoutes.js +557 -0
  98. package/routes/authRoutes.js +2310 -0
  99. package/routes/avatarRoutes.js +85 -0
  100. package/routes/botRoutes.js +211 -0
  101. package/routes/chatRoutes.js +377 -0
  102. package/routes/cryptoPriceRoutes.js +105 -0
  103. package/routes/developerRoutes.js +4201 -0
  104. package/routes/deviceRoutes.js +214 -0
  105. package/routes/dmRoutes.js +167 -0
  106. package/routes/esportsRoutes.js +806 -0
  107. package/routes/exchangeRateRoutes.js +233 -0
  108. package/routes/gamesRoutes.js +3028 -0
  109. package/routes/jackpotRoutes.js +754 -0
  110. package/routes/keeperMonitoringRoutes.js +156 -0
  111. package/routes/keeperWebhookRoutes.js +466 -0
  112. package/routes/livescoresRoutes.js +31 -0
  113. package/routes/pickemAdminRoutes.js +199 -0
  114. package/routes/pickemRoutes.js +231 -0
  115. package/routes/playerStatsRoutes.js +147 -0
  116. package/routes/portfolioRoutes.js +217 -0
  117. package/routes/promoRoutes.js +418 -0
  118. package/routes/referralEarningsRoutes.js +392 -0
  119. package/routes/socialRoutes.js +459 -0
  120. package/routes/sportsRoutes.js +1271 -0
  121. package/routes/survivorAdminRoutes.js +345 -0
  122. package/routes/survivorRoutes.js +756 -0
  123. package/routes/uploadRoutes.js +256 -0
  124. package/routes/userProfileRoutes.js +244 -0
  125. package/routes/whatsNewRoutes.js +331 -0
  126. package/scripts/.claude/settings.local.json +15 -0
  127. package/scripts/README.md +170 -0
  128. package/scripts/RESTART_EVERYTHING.sh +104 -0
  129. package/scripts/add-claim-columns.sql +48 -0
  130. package/scripts/add-crypto-prices-cache.sql +27 -0
  131. package/scripts/add-exchange-rates-cache.sql +40 -0
  132. package/scripts/add-game-invite-column.sql +23 -0
  133. package/scripts/add-game-invite-notification.sql +33 -0
  134. package/scripts/add-game-invite-telegram-pref.sql +16 -0
  135. package/scripts/add-game-joined-notification.sql +16 -0
  136. package/scripts/add-game-joined-pref.js +40 -0
  137. package/scripts/add-game-joined-preference.sql +6 -0
  138. package/scripts/add-game-start-notifications.sql +41 -0
  139. package/scripts/add-notification-flags-to-games.sql +55 -0
  140. package/scripts/add-pending-game-dismissals.sql +19 -0
  141. package/scripts/add-preferred-currency.sql +34 -0
  142. package/scripts/add-winner-columns.js +61 -0
  143. package/scripts/add_mention_system.sql +53 -0
  144. package/scripts/add_payment_system.sql +96 -0
  145. package/scripts/add_sports_event_id_column.sql +22 -0
  146. package/scripts/analyze-cohort-data-heroku.js +276 -0
  147. package/scripts/analyze-cohort-data.js +295 -0
  148. package/scripts/analyze-prod-cohorts.sh +10 -0
  149. package/scripts/backfill-matchup-images.js +245 -0
  150. package/scripts/backfill-missing-signatures.js +175 -0
  151. package/scripts/backfill-referral-earnings.js +202 -0
  152. package/scripts/check-chat-schema.js +130 -0
  153. package/scripts/check-db.sh +14 -0
  154. package/scripts/check_oracle_in_game.js +54 -0
  155. package/scripts/cleanup-database.js +193 -0
  156. package/scripts/clear-notification-cache.js +85 -0
  157. package/scripts/convert-mnemonic.js +50 -0
  158. package/scripts/create-users-table.sql +44 -0
  159. package/scripts/debug-cohort-counts.js +248 -0
  160. package/scripts/debug-winner-calc.js +84 -0
  161. package/scripts/deploy-payment-system.sh +118 -0
  162. package/scripts/deploy-to-heroku.sh +63 -0
  163. package/scripts/diagnose-locked-round.js +143 -0
  164. package/scripts/dubs-cli.js +720 -0
  165. package/scripts/dump-account.js +65 -0
  166. package/scripts/find-vrf-offset.js +48 -0
  167. package/scripts/fix-chat-notifications-constraint.sql +122 -0
  168. package/scripts/fix-claim-columns.js +124 -0
  169. package/scripts/fix-constraint-now.js +44 -0
  170. package/scripts/fix-lock-timestamps.js +96 -0
  171. package/scripts/fix-locked-round.sh +126 -0
  172. package/scripts/fix-missing-badges.sql +91 -0
  173. package/scripts/fix-payment-notifications.sql +41 -0
  174. package/scripts/force-new-round.js +55 -0
  175. package/scripts/force-resolve-and-claim.js +278 -0
  176. package/scripts/important/README.md +115 -0
  177. package/scripts/important/authority-force-lock.js +197 -0
  178. package/scripts/important/authority-resolve-game.js +267 -0
  179. package/scripts/important/check-game-status.js +373 -0
  180. package/scripts/important/list-pending-games-by-version.js +270 -0
  181. package/scripts/important/reconcile-v1-v2-payouts.js +270 -0
  182. package/scripts/initialize-jackpot.js +111 -0
  183. package/scripts/jackpot/.claude/settings.local.json +10 -0
  184. package/scripts/jackpot/force-reset.js +84 -0
  185. package/scripts/jackpot/initialize-mainnet.js +100 -0
  186. package/scripts/jackpot/keeper.js +742 -0
  187. package/scripts/jackpot/status.js +107 -0
  188. package/scripts/jackpot/update-round-duration.js +143 -0
  189. package/scripts/keeper-bot.js +112 -0
  190. package/scripts/list-pending-games.js +131 -0
  191. package/scripts/migrate-chat-v2.js +127 -0
  192. package/scripts/migrate-chat-winners.js +84 -0
  193. package/scripts/migrate-chat.sh +17 -0
  194. package/scripts/migrate-game-invite.js +83 -0
  195. package/scripts/migrate-heroku-game-notifications.sh +159 -0
  196. package/scripts/migrations/001_analytics_tables.sql +422 -0
  197. package/scripts/migrations/002_add_matchup_image_url.sql +14 -0
  198. package/scripts/migrations/003_referral_earnings.sql +208 -0
  199. package/scripts/migrations/004_add_whats_new_notification_type.sql +62 -0
  200. package/scripts/migrations/005_add_connect4_your_turn_notification.sql +61 -0
  201. package/scripts/migrations/005_push_notifications.sql +55 -0
  202. package/scripts/migrations/006_add_draw_team_players.sql +28 -0
  203. package/scripts/migrations/006_add_game_cancelled_notification.sql +62 -0
  204. package/scripts/migrations/007_add_gif_url.sql +8 -0
  205. package/scripts/migrations/008_add_connect4_columns.sql +139 -0
  206. package/scripts/migrations/008_add_pool_tracking.sql +22 -0
  207. package/scripts/migrations/009_create_survivor_pool_tables.sql +174 -0
  208. package/scripts/migrations/010_add_survivor_pool_outcome.sql +28 -0
  209. package/scripts/migrations/011_create_developer_tables.sql +67 -0
  210. package/scripts/migrations/011_fix_keeper_tables.sql +85 -0
  211. package/scripts/migrations/012_create_developer_webhooks.sql +31 -0
  212. package/scripts/migrations/013_add_network_mode.sql +18 -0
  213. package/scripts/migrations/014_create_developer_app_users.sql +19 -0
  214. package/scripts/migrations/015_add_ui_config.sql +4 -0
  215. package/scripts/migrations/016_add_resolution_secret.sql +4 -0
  216. package/scripts/migrations/017_add_external_game_id.sql +3 -0
  217. package/scripts/migrations/018_create_pickem_tables.sql +115 -0
  218. package/scripts/migrations/019_expo_push_tokens.sql +19 -0
  219. package/scripts/migrations/create_whats_new_tables.sql +88 -0
  220. package/scripts/migrations/drop_live_games_tables.sql +34 -0
  221. package/scripts/open-jackpot-round.js +85 -0
  222. package/scripts/purge-all-data.sh +329 -0
  223. package/scripts/purge-all-data.sql +142 -0
  224. package/scripts/purge-heroku-data.sh +149 -0
  225. package/scripts/purge-heroku-data.sql +62 -0
  226. package/scripts/rebuild-heroku-database.sh +113 -0
  227. package/scripts/recover-funds.js +357 -0
  228. package/scripts/regenerate-epl-images.js +278 -0
  229. package/scripts/resize-s3-matchup-images.js +374 -0
  230. package/scripts/resolve-direct.js +88 -0
  231. package/scripts/resolve-mock-game.js +124 -0
  232. package/scripts/resolve-pickem-game.js +55 -0
  233. package/scripts/resolve-round-manual.js +83 -0
  234. package/scripts/resolve-stuck-game.js +382 -0
  235. package/scripts/resolve-stuck-round.js +42 -0
  236. package/scripts/run-connect4-migration.sh +16 -0
  237. package/scripts/run-mention-migration.sh +32 -0
  238. package/scripts/run-payment-migration.sh +51 -0
  239. package/scripts/run-preferred-currency-migration.sh +31 -0
  240. package/scripts/run-referral-earnings-migration.sh +32 -0
  241. package/scripts/run-survivor-outcome-migration.sh +16 -0
  242. package/scripts/seed-test-users.js +346 -0
  243. package/scripts/setup-auth-tables.js +78 -0
  244. package/scripts/setup-complete-database.sql +992 -0
  245. package/scripts/setup-database-fresh.sh +359 -0
  246. package/scripts/setup-heroku-keeper.sh +48 -0
  247. package/scripts/setup-keeper-database.js +83 -0
  248. package/scripts/setup-keeper-state-db.sql +110 -0
  249. package/scripts/setup-oracle.sh +39 -0
  250. package/scripts/setup-pnl-tracking.js +111 -0
  251. package/scripts/start-devnet.sh +14 -0
  252. package/scripts/test-arcade-devnet.sh +160 -0
  253. package/scripts/test-arcade-match.sh +109 -0
  254. package/scripts/test-automatic-mode.sh +239 -0
  255. package/scripts/test-connect4-cancel-claim.js +370 -0
  256. package/scripts/test-connect4-e2e.js +369 -0
  257. package/scripts/test-connect4-resolve.js +369 -0
  258. package/scripts/test-game-state-endpoint.js +136 -0
  259. package/scripts/test-invite-notification.js +86 -0
  260. package/scripts/test-jackpot-api.sh +71 -0
  261. package/scripts/test-poll-confirmation.js +267 -0
  262. package/scripts/test-resolve-game.js +271 -0
  263. package/scripts/test-resolve-signature.js +223 -0
  264. package/scripts/test-signature-preservation.js +124 -0
  265. package/scripts/test-state-machine.js +291 -0
  266. package/scripts/test-webhook-receiver.js +60 -0
  267. package/scripts/update-notification-constraint.js +52 -0
  268. package/scripts/verify-account-layout.js +145 -0
  269. package/scripts/verify-winner-algorithm.js +278 -0
  270. package/server.js +5259 -0
  271. package/services/arcadeMatchService.js +763 -0
  272. package/services/automaticGameOracle.js +1596 -0
  273. package/services/chatService.js +1612 -0
  274. package/services/connect4GameService.js +1049 -0
  275. package/services/connect4NotificationService.js +374 -0
  276. package/services/cryptoPriceService.js +223 -0
  277. package/services/customGameResolver.js +260 -0
  278. package/services/db.js +79 -0
  279. package/services/directMessageService.js +389 -0
  280. package/services/discordNotifications.js +160 -0
  281. package/services/exchangeRateService.js +289 -0
  282. package/services/expoPushService.js +314 -0
  283. package/services/gamesCacheService.js +539 -0
  284. package/services/jackpotHistory.js +331 -0
  285. package/services/jackpotService.js +856 -0
  286. package/services/keeperStateService.js +355 -0
  287. package/services/matchupImageService.js +591 -0
  288. package/services/notificationCacheService.js +407 -0
  289. package/services/pickemOracle.js +440 -0
  290. package/services/playerStatsService.js +389 -0
  291. package/services/portfolioService.js +555 -0
  292. package/services/promoService.js +757 -0
  293. package/services/promoTreasuryService.js +239 -0
  294. package/services/pushNotifications.js +353 -0
  295. package/services/redisService.js +422 -0
  296. package/services/referralEarningsService.js +728 -0
  297. package/services/s3Service.js +396 -0
  298. package/services/socialService.js +1202 -0
  299. package/services/survivorOracle.js +469 -0
  300. package/services/survivorSimulator.js +475 -0
  301. package/services/telegramNotifications.js +461 -0
  302. package/services/userProfileStatsService.js +1185 -0
  303. package/services/whatsNewService.js +388 -0
  304. 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
+