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,539 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Games Cache Service - Redis-backed high-performance games list layer
|
|
3
|
+
*
|
|
4
|
+
* Architecture (per-user, like notificationCacheService):
|
|
5
|
+
* - Redis Sorted Set: user_games:{walletAddress} → game IDs sorted by joinedAt timestamp
|
|
6
|
+
* - Redis Hash: user_game:{walletAddress}:{gameId} → trimmed game data (list-view fields only)
|
|
7
|
+
* - No TTL: cache persists until explicitly invalidated. Kept in sync via write-through
|
|
8
|
+
* on create, join, resolve, and claim operations.
|
|
9
|
+
*
|
|
10
|
+
* Each user gets their own copy of game data (includes user-specific fields like
|
|
11
|
+
* claimedAt, teamChoice, etc.). This matches the notificationCacheService pattern
|
|
12
|
+
* and avoids cross-user concerns.
|
|
13
|
+
*
|
|
14
|
+
* Gracefully falls back to PostgreSQL when Redis is unavailable.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const redisService = require('./redisService');
|
|
18
|
+
|
|
19
|
+
// Key prefixes
|
|
20
|
+
const KEYS = {
|
|
21
|
+
USER_GAMES: (wallet) => `user_games:${wallet}`,
|
|
22
|
+
GAME: (wallet, gameId) => `user_game:${wallet}:${gameId}`,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Maximum games to cache per user
|
|
26
|
+
const MAX_CACHED_GAMES = 50;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Fields stored in the game hash (trimmed from the full 30+ field response).
|
|
30
|
+
* Complex objects (arrays, objects) are JSON-stringified.
|
|
31
|
+
*/
|
|
32
|
+
function serializeGameForCache(gameData) {
|
|
33
|
+
return {
|
|
34
|
+
gameId: gameData.gameId || '',
|
|
35
|
+
gameAddress: gameData.gameAddress || '',
|
|
36
|
+
title: gameData.title || '',
|
|
37
|
+
matchupImageUrl: gameData.matchupImageUrl || '',
|
|
38
|
+
imageUrl: gameData.imageUrl || '',
|
|
39
|
+
gameType: gameData.gameType || '',
|
|
40
|
+
buyIn: (gameData.buyIn ?? 0).toString(),
|
|
41
|
+
gameMode: (gameData.gameMode ?? 0).toString(),
|
|
42
|
+
createdBy: gameData.createdBy || '',
|
|
43
|
+
teamChoice: gameData.teamChoice || '',
|
|
44
|
+
claimedAt: gameData.claimedAt || '',
|
|
45
|
+
isLocked: gameData.isLocked ? '1' : '0',
|
|
46
|
+
isResolved: gameData.isResolved ? '1' : '0',
|
|
47
|
+
automaticStatus: gameData.automaticStatus || '',
|
|
48
|
+
status: gameData.status || '',
|
|
49
|
+
lockTimestamp: (gameData.lockTimestamp ?? '').toString(),
|
|
50
|
+
maxPlayers: (gameData.maxPlayers ?? 0).toString(),
|
|
51
|
+
// Arrays → JSON
|
|
52
|
+
homeTeamPlayers: JSON.stringify(gameData.homeTeamPlayers || []),
|
|
53
|
+
awayTeamPlayers: JSON.stringify(gameData.awayTeamPlayers || []),
|
|
54
|
+
drawTeamPlayers: JSON.stringify(gameData.drawTeamPlayers || []),
|
|
55
|
+
players: JSON.stringify(gameData.players || []),
|
|
56
|
+
// Extracted from sportsEvent (NOT the full blob)
|
|
57
|
+
strLeague: gameData.sportsEvent?.strLeague || gameData.strLeague || '',
|
|
58
|
+
strHomeTeam: gameData.sportsEvent?.strHomeTeam || gameData.homeTeam || '',
|
|
59
|
+
strAwayTeam: gameData.sportsEvent?.strAwayTeam || gameData.awayTeam || '',
|
|
60
|
+
strHomeTeamBadge: gameData.sportsEvent?.strHomeTeamBadge || gameData.homeTeamBadge || '',
|
|
61
|
+
strAwayTeamBadge: gameData.sportsEvent?.strAwayTeamBadge || gameData.awayTeamBadge || '',
|
|
62
|
+
strTimestamp: gameData.sportsEvent?.strTimestamp || gameData.strTimestamp || '',
|
|
63
|
+
// Final score fields
|
|
64
|
+
finalScoreWinner: gameData.finalScore?.winner ?? '',
|
|
65
|
+
finalScoreHome: (gameData.finalScore?.homeScore ?? '').toString(),
|
|
66
|
+
finalScoreAway: (gameData.finalScore?.awayScore ?? '').toString(),
|
|
67
|
+
// Connect4
|
|
68
|
+
winner: gameData.winner || '',
|
|
69
|
+
connect4Winner: gameData.connect4Winner || '',
|
|
70
|
+
// User-specific fields
|
|
71
|
+
role: gameData.role || '',
|
|
72
|
+
// CRITICAL: Convert Date objects to ISO strings before storing in Redis.
|
|
73
|
+
// Redis HSET calls .toString() on values, which produces "Thu Feb 12 2026 ..."
|
|
74
|
+
// format instead of ISO "2026-02-12T..." format. This caused inconsistent
|
|
75
|
+
// date formats between cache-served and DB-served responses.
|
|
76
|
+
joinedAt: gameData.joinedAt instanceof Date
|
|
77
|
+
? gameData.joinedAt.toISOString()
|
|
78
|
+
: (gameData.joinedAt || ''),
|
|
79
|
+
mySignature: gameData.mySignature || '',
|
|
80
|
+
myExplorerUrl: gameData.myExplorerUrl || '',
|
|
81
|
+
walletType: gameData.walletType || '',
|
|
82
|
+
claimSignature: gameData.claimSignature || '',
|
|
83
|
+
claimExplorerUrl: gameData.claimExplorerUrl || '',
|
|
84
|
+
amountClaimed: (gameData.amountClaimed ?? '').toString(),
|
|
85
|
+
// Room name for billiards
|
|
86
|
+
roomName: gameData.roomName || '',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function deserializeGameFromCache(data) {
|
|
91
|
+
const finalScoreWinner = data.finalScoreWinner;
|
|
92
|
+
const finalScoreHome = data.finalScoreHome;
|
|
93
|
+
const finalScoreAway = data.finalScoreAway;
|
|
94
|
+
|
|
95
|
+
let finalScore = null;
|
|
96
|
+
if (finalScoreWinner !== '' || finalScoreHome !== '' || finalScoreAway !== '') {
|
|
97
|
+
finalScore = {
|
|
98
|
+
winner: finalScoreWinner === '' ? null : finalScoreWinner,
|
|
99
|
+
homeScore: finalScoreHome !== '' ? parseInt(finalScoreHome) || 0 : undefined,
|
|
100
|
+
awayScore: finalScoreAway !== '' ? parseInt(finalScoreAway) || 0 : undefined,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Rebuild a minimal sportsEvent from extracted fields
|
|
105
|
+
const sportsEvent = {};
|
|
106
|
+
if (data.strLeague) sportsEvent.strLeague = data.strLeague;
|
|
107
|
+
if (data.strHomeTeam) sportsEvent.strHomeTeam = data.strHomeTeam;
|
|
108
|
+
if (data.strAwayTeam) sportsEvent.strAwayTeam = data.strAwayTeam;
|
|
109
|
+
if (data.strHomeTeamBadge) sportsEvent.strHomeTeamBadge = data.strHomeTeamBadge;
|
|
110
|
+
if (data.strAwayTeamBadge) sportsEvent.strAwayTeamBadge = data.strAwayTeamBadge;
|
|
111
|
+
if (data.strTimestamp) sportsEvent.strTimestamp = data.strTimestamp;
|
|
112
|
+
if (finalScore) sportsEvent.finalScore = finalScore;
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
gameId: data.gameId,
|
|
116
|
+
gameAddress: data.gameAddress || null,
|
|
117
|
+
title: data.title || '',
|
|
118
|
+
imageUrl: data.imageUrl || null,
|
|
119
|
+
matchupImageUrl: data.matchupImageUrl || null,
|
|
120
|
+
gameType: data.gameType || '',
|
|
121
|
+
buyIn: parseFloat(data.buyIn) || 0,
|
|
122
|
+
maxPlayers: parseInt(data.maxPlayers) || 0,
|
|
123
|
+
gameMode: parseInt(data.gameMode) || 0,
|
|
124
|
+
createdBy: data.createdBy || null,
|
|
125
|
+
sportsEvent: Object.keys(sportsEvent).length > 0 ? sportsEvent : null,
|
|
126
|
+
homeTeam: data.strHomeTeam || undefined,
|
|
127
|
+
awayTeam: data.strAwayTeam || undefined,
|
|
128
|
+
league: data.strLeague || undefined,
|
|
129
|
+
homeTeamBadge: data.strHomeTeamBadge || undefined,
|
|
130
|
+
awayTeamBadge: data.strAwayTeamBadge || undefined,
|
|
131
|
+
strTimestamp: data.strTimestamp || undefined,
|
|
132
|
+
homeTeamPlayers: safeJsonParse(data.homeTeamPlayers) || [],
|
|
133
|
+
awayTeamPlayers: safeJsonParse(data.awayTeamPlayers) || [],
|
|
134
|
+
drawTeamPlayers: safeJsonParse(data.drawTeamPlayers) || [],
|
|
135
|
+
players: safeJsonParse(data.players) || [],
|
|
136
|
+
roomName: data.roomName || undefined,
|
|
137
|
+
lockTimestamp: data.lockTimestamp ? parseInt(data.lockTimestamp) || null : null,
|
|
138
|
+
isLocked: data.isLocked === '1',
|
|
139
|
+
isResolved: data.isResolved === '1',
|
|
140
|
+
automaticStatus: data.automaticStatus || null,
|
|
141
|
+
finalScore,
|
|
142
|
+
winner: data.winner || null,
|
|
143
|
+
connect4Winner: data.connect4Winner || null,
|
|
144
|
+
status: data.status || null,
|
|
145
|
+
// User-specific
|
|
146
|
+
role: data.role || null,
|
|
147
|
+
joinedAt: data.joinedAt || null,
|
|
148
|
+
teamChoice: data.teamChoice || null,
|
|
149
|
+
mySignature: data.mySignature || null,
|
|
150
|
+
myExplorerUrl: data.myExplorerUrl || null,
|
|
151
|
+
walletType: data.walletType || null,
|
|
152
|
+
claimedAt: data.claimedAt || null,
|
|
153
|
+
claimSignature: data.claimSignature || null,
|
|
154
|
+
claimExplorerUrl: data.claimExplorerUrl || null,
|
|
155
|
+
amountClaimed: data.amountClaimed ? parseFloat(data.amountClaimed) : null,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function safeJsonParse(str) {
|
|
160
|
+
if (!str) return null;
|
|
161
|
+
try {
|
|
162
|
+
return JSON.parse(str);
|
|
163
|
+
} catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
class GamesCacheService {
|
|
169
|
+
constructor() {
|
|
170
|
+
this.enabled = false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Initialize the cache service
|
|
175
|
+
* Called during server startup (after redisService.connect())
|
|
176
|
+
*/
|
|
177
|
+
async initialize() {
|
|
178
|
+
// Redis connection is already established by notificationCacheService
|
|
179
|
+
this.enabled = redisService.isAvailable();
|
|
180
|
+
if (this.enabled) {
|
|
181
|
+
console.log('[GamesCache] Redis caching enabled');
|
|
182
|
+
} else {
|
|
183
|
+
console.log('[GamesCache] Running without Redis cache (PostgreSQL fallback)');
|
|
184
|
+
}
|
|
185
|
+
return this.enabled;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
isEnabled() {
|
|
189
|
+
return this.enabled && redisService.isAvailable();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ============================================
|
|
193
|
+
// WRITE OPERATIONS
|
|
194
|
+
// ============================================
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Cache a game for a specific user.
|
|
198
|
+
* Called after saving a game to PostgreSQL.
|
|
199
|
+
*/
|
|
200
|
+
async cacheGame(walletAddress, gameData) {
|
|
201
|
+
if (!this.isEnabled()) return false;
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const gameId = gameData.gameId;
|
|
205
|
+
const timestamp = gameData.joinedAt
|
|
206
|
+
? new Date(gameData.joinedAt).getTime()
|
|
207
|
+
: Date.now();
|
|
208
|
+
|
|
209
|
+
const pipeline = redisService.pipeline();
|
|
210
|
+
if (!pipeline) return false;
|
|
211
|
+
|
|
212
|
+
// 1. Add to user's sorted set
|
|
213
|
+
pipeline.zadd(KEYS.USER_GAMES(walletAddress), timestamp, gameId);
|
|
214
|
+
|
|
215
|
+
// 2. Store game data as hash
|
|
216
|
+
pipeline.hset(KEYS.GAME(walletAddress, gameId), serializeGameForCache(gameData));
|
|
217
|
+
|
|
218
|
+
// 3. Trim sorted set to max size
|
|
219
|
+
pipeline.zremrangebyrank(KEYS.USER_GAMES(walletAddress), 0, -(MAX_CACHED_GAMES + 1));
|
|
220
|
+
|
|
221
|
+
await pipeline.exec();
|
|
222
|
+
|
|
223
|
+
console.log(`[GamesCache] Cached game ${gameId} for ${walletAddress.slice(0, 8)}...`);
|
|
224
|
+
return true;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error('[GamesCache] Error caching game:', error.message);
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Partial update of a game hash for a specific user.
|
|
233
|
+
* Only updates the fields provided.
|
|
234
|
+
*/
|
|
235
|
+
async updateGame(walletAddress, gameId, fields) {
|
|
236
|
+
if (!this.isEnabled()) return false;
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const key = KEYS.GAME(walletAddress, gameId);
|
|
240
|
+
|
|
241
|
+
// Check if hash exists
|
|
242
|
+
const exists = await redisService.exists(key);
|
|
243
|
+
if (!exists) return false;
|
|
244
|
+
|
|
245
|
+
// Build partial update object
|
|
246
|
+
const update = {};
|
|
247
|
+
if (fields.isLocked !== undefined) update.isLocked = fields.isLocked ? '1' : '0';
|
|
248
|
+
if (fields.isResolved !== undefined) update.isResolved = fields.isResolved ? '1' : '0';
|
|
249
|
+
if (fields.automaticStatus !== undefined) update.automaticStatus = fields.automaticStatus;
|
|
250
|
+
if (fields.status !== undefined) update.status = fields.status;
|
|
251
|
+
if (fields.claimedAt !== undefined) update.claimedAt = fields.claimedAt || '';
|
|
252
|
+
if (fields.claimSignature !== undefined) update.claimSignature = fields.claimSignature || '';
|
|
253
|
+
if (fields.claimExplorerUrl !== undefined) update.claimExplorerUrl = fields.claimExplorerUrl || '';
|
|
254
|
+
if (fields.amountClaimed !== undefined) update.amountClaimed = (fields.amountClaimed ?? '').toString();
|
|
255
|
+
if (fields.finalScoreWinner !== undefined) update.finalScoreWinner = fields.finalScoreWinner ?? '';
|
|
256
|
+
if (fields.finalScoreHome !== undefined) update.finalScoreHome = (fields.finalScoreHome ?? '').toString();
|
|
257
|
+
if (fields.finalScoreAway !== undefined) update.finalScoreAway = (fields.finalScoreAway ?? '').toString();
|
|
258
|
+
if (fields.homeTeamPlayers !== undefined) update.homeTeamPlayers = JSON.stringify(fields.homeTeamPlayers);
|
|
259
|
+
if (fields.awayTeamPlayers !== undefined) update.awayTeamPlayers = JSON.stringify(fields.awayTeamPlayers);
|
|
260
|
+
if (fields.drawTeamPlayers !== undefined) update.drawTeamPlayers = JSON.stringify(fields.drawTeamPlayers);
|
|
261
|
+
if (fields.winner !== undefined) update.winner = fields.winner || '';
|
|
262
|
+
if (fields.connect4Winner !== undefined) update.connect4Winner = fields.connect4Winner || '';
|
|
263
|
+
|
|
264
|
+
if (Object.keys(update).length === 0) return false;
|
|
265
|
+
|
|
266
|
+
await redisService.hset(key, update);
|
|
267
|
+
return true;
|
|
268
|
+
} catch (error) {
|
|
269
|
+
console.error('[GamesCache] Error updating game:', error.message);
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Update a game for ALL users who have it cached.
|
|
276
|
+
* Used for shared state changes (lock, resolve) that affect all participants.
|
|
277
|
+
*/
|
|
278
|
+
async updateGameForAllUsers(gameId, playerWallets, fields) {
|
|
279
|
+
if (!this.isEnabled()) return false;
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
for (const wallet of playerWallets) {
|
|
283
|
+
await this.updateGame(wallet, gameId, fields);
|
|
284
|
+
}
|
|
285
|
+
return true;
|
|
286
|
+
} catch (error) {
|
|
287
|
+
console.error('[GamesCache] Error updating game for all users:', error.message);
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Add a player to a game (when they join).
|
|
294
|
+
* Adds the game to the joiner's sorted set and creates their game hash.
|
|
295
|
+
* Also updates the team arrays in existing users' game hashes.
|
|
296
|
+
*/
|
|
297
|
+
async addPlayerToGame(gameId, walletAddress, gameData, existingPlayerWallets) {
|
|
298
|
+
if (!this.isEnabled()) return false;
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
// 1. Cache the game for the joining user
|
|
302
|
+
await this.cacheGame(walletAddress, gameData);
|
|
303
|
+
|
|
304
|
+
// 2. Update team arrays for all existing players
|
|
305
|
+
if (existingPlayerWallets && existingPlayerWallets.length > 0) {
|
|
306
|
+
const teamUpdate = {};
|
|
307
|
+
if (gameData.homeTeamPlayers) teamUpdate.homeTeamPlayers = gameData.homeTeamPlayers;
|
|
308
|
+
if (gameData.awayTeamPlayers) teamUpdate.awayTeamPlayers = gameData.awayTeamPlayers;
|
|
309
|
+
if (gameData.drawTeamPlayers) teamUpdate.drawTeamPlayers = gameData.drawTeamPlayers;
|
|
310
|
+
|
|
311
|
+
for (const existingWallet of existingPlayerWallets) {
|
|
312
|
+
if (existingWallet !== walletAddress) {
|
|
313
|
+
await this.updateGame(existingWallet, gameId, teamUpdate);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return true;
|
|
319
|
+
} catch (error) {
|
|
320
|
+
console.error('[GamesCache] Error adding player to game:', error.message);
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ============================================
|
|
326
|
+
// READ OPERATIONS
|
|
327
|
+
// ============================================
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Get paginated games from cache.
|
|
331
|
+
* Uses cursor-based pagination with timestamps.
|
|
332
|
+
*
|
|
333
|
+
* @returns {Object|null} - { games, nextCursor, hasMore } or null on cache miss
|
|
334
|
+
*/
|
|
335
|
+
async getGames(walletAddress, { limit = 10, cursor = null } = {}) {
|
|
336
|
+
if (!this.isEnabled()) return null;
|
|
337
|
+
|
|
338
|
+
// CRITICAL: Only serve the FIRST page from cache. Cursor (Load More) pages
|
|
339
|
+
// MUST always go to PostgreSQL. The cache is inherently incomplete — it holds
|
|
340
|
+
// at most MAX_CACHED_GAMES (50) items, warmed from the first page only.
|
|
341
|
+
// Serving cursor pages from an incomplete cache causes "missing games" where
|
|
342
|
+
// Load More returns a handful of games then stops, hiding hundreds of games.
|
|
343
|
+
if (cursor) {
|
|
344
|
+
console.log(`[GamesCache] Cursor page — bypassing cache for ${walletAddress.slice(0, 8)}...`);
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
// Check if user has cached games
|
|
350
|
+
const exists = await redisService.exists(KEYS.USER_GAMES(walletAddress));
|
|
351
|
+
if (!exists) {
|
|
352
|
+
console.log(`[GamesCache] Cache miss for ${walletAddress.slice(0, 8)}...`);
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Check total cached count to know if cache is complete enough to serve first page
|
|
357
|
+
const totalCached = await redisService.zcard(KEYS.USER_GAMES(walletAddress));
|
|
358
|
+
|
|
359
|
+
// Get game IDs from sorted set (first page only)
|
|
360
|
+
let gameIds = await redisService.zrevrange(
|
|
361
|
+
KEYS.USER_GAMES(walletAddress),
|
|
362
|
+
0,
|
|
363
|
+
limit // Returns limit+1 items (indices 0 through limit, inclusive)
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
if (!gameIds || gameIds.length === 0) {
|
|
367
|
+
return { games: [], nextCursor: null, hasMore: false };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// If we don't have more than limit items cached, cache may be incomplete
|
|
371
|
+
// (warmed from first page only). Fall through to PostgreSQL.
|
|
372
|
+
const hasMore = gameIds.length > limit;
|
|
373
|
+
if (!hasMore && totalCached <= limit) {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
if (hasMore) {
|
|
377
|
+
gameIds = gameIds.slice(0, limit);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Batch fetch game data
|
|
381
|
+
const gameKeys = gameIds.map(id => KEYS.GAME(walletAddress, id));
|
|
382
|
+
const gamesData = await redisService.hmgetall(gameKeys);
|
|
383
|
+
|
|
384
|
+
if (!gamesData) {
|
|
385
|
+
return null; // Cache miss on individual games
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Check for missing game hashes (orphaned sorted set entries).
|
|
389
|
+
// If ANY hashes are missing, the cache is inconsistent — fall through to DB.
|
|
390
|
+
const missingCount = gamesData.filter(data => data === null).length;
|
|
391
|
+
if (missingCount > 0) {
|
|
392
|
+
console.log(`[GamesCache] ${missingCount}/${gameIds.length} game hashes missing for ${walletAddress.slice(0, 8)}... — cache inconsistent, falling through to DB`);
|
|
393
|
+
// Clean up orphaned sorted set entries in the background
|
|
394
|
+
const orphanedIds = gameIds.filter((id, i) => gamesData[i] === null);
|
|
395
|
+
this._cleanupOrphanedEntries(walletAddress, orphanedIds).catch(() => {});
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Parse and format games
|
|
400
|
+
const games = gamesData
|
|
401
|
+
.map(data => deserializeGameFromCache(data));
|
|
402
|
+
|
|
403
|
+
// Get next cursor (timestamp of last item)
|
|
404
|
+
let nextCursor = null;
|
|
405
|
+
if (hasMore && games.length > 0) {
|
|
406
|
+
const lastGame = games[games.length - 1];
|
|
407
|
+
const lastGameId = lastGame.gameId;
|
|
408
|
+
// Get the score (timestamp) for the last game
|
|
409
|
+
const score = await redisService.zscore(KEYS.USER_GAMES(walletAddress), lastGameId);
|
|
410
|
+
if (score) {
|
|
411
|
+
nextCursor = score.toString();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return { games, nextCursor, hasMore };
|
|
416
|
+
} catch (error) {
|
|
417
|
+
console.error('[GamesCache] Error getting games:', error.message);
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Bulk populate cache from PostgreSQL (called on first cache miss).
|
|
424
|
+
*/
|
|
425
|
+
async warmCache(walletAddress, games) {
|
|
426
|
+
if (!this.isEnabled() || games.length === 0) return false;
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
const pipeline = redisService.pipeline();
|
|
430
|
+
if (!pipeline) return false;
|
|
431
|
+
|
|
432
|
+
for (const game of games) {
|
|
433
|
+
const gameId = game.gameId;
|
|
434
|
+
const timestamp = game.joinedAt
|
|
435
|
+
? new Date(game.joinedAt).getTime()
|
|
436
|
+
: Date.now();
|
|
437
|
+
|
|
438
|
+
pipeline.zadd(KEYS.USER_GAMES(walletAddress), timestamp, gameId);
|
|
439
|
+
pipeline.hset(KEYS.GAME(walletAddress, gameId), serializeGameForCache(game));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Trim to max size
|
|
443
|
+
pipeline.zremrangebyrank(KEYS.USER_GAMES(walletAddress), 0, -(MAX_CACHED_GAMES + 1));
|
|
444
|
+
|
|
445
|
+
await pipeline.exec();
|
|
446
|
+
|
|
447
|
+
console.log(`[GamesCache] Warmed cache with ${games.length} games for ${walletAddress.slice(0, 8)}...`);
|
|
448
|
+
return true;
|
|
449
|
+
} catch (error) {
|
|
450
|
+
console.error('[GamesCache] Error warming cache:', error.message);
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Invalidate user's game cache.
|
|
457
|
+
*/
|
|
458
|
+
async invalidateUserCache(walletAddress) {
|
|
459
|
+
if (!this.isEnabled()) return false;
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
const gameIds = await redisService.zrevrange(KEYS.USER_GAMES(walletAddress), 0, -1);
|
|
463
|
+
|
|
464
|
+
if (gameIds && gameIds.length > 0) {
|
|
465
|
+
const keysToDelete = gameIds.map(id => KEYS.GAME(walletAddress, id));
|
|
466
|
+
keysToDelete.push(KEYS.USER_GAMES(walletAddress));
|
|
467
|
+
await redisService.del(...keysToDelete);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
console.log(`[GamesCache] Invalidated cache for ${walletAddress.slice(0, 8)}...`);
|
|
471
|
+
return true;
|
|
472
|
+
} catch (error) {
|
|
473
|
+
console.error('[GamesCache] Error invalidating cache:', error.message);
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Flush ALL games cache data from Redis.
|
|
480
|
+
* Safe to call in production — all requests will fall through to PostgreSQL
|
|
481
|
+
* and re-warm the cache with correct data on next access.
|
|
482
|
+
* @returns {{ deletedKeys: number }} - Number of keys deleted
|
|
483
|
+
*/
|
|
484
|
+
async flushAllGamesCaches() {
|
|
485
|
+
if (!this.isEnabled()) return { deletedKeys: 0 };
|
|
486
|
+
|
|
487
|
+
try {
|
|
488
|
+
// Use SCAN to find all games cache keys (non-blocking, safe for production)
|
|
489
|
+
const sortedSetKeys = await redisService.scanKeys('user_games:*');
|
|
490
|
+
const hashKeys = await redisService.scanKeys('user_game:*');
|
|
491
|
+
const allKeys = [...sortedSetKeys, ...hashKeys];
|
|
492
|
+
|
|
493
|
+
if (allKeys.length === 0) {
|
|
494
|
+
console.log('[GamesCache] No games cache keys found to flush');
|
|
495
|
+
return { deletedKeys: 0 };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Delete in batches of 500 to avoid overloading Redis
|
|
499
|
+
const batchSize = 500;
|
|
500
|
+
for (let i = 0; i < allKeys.length; i += batchSize) {
|
|
501
|
+
const batch = allKeys.slice(i, i + batchSize);
|
|
502
|
+
await redisService.del(...batch);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
console.log(`[GamesCache] Flushed ${allKeys.length} keys (${sortedSetKeys.length} sorted sets, ${hashKeys.length} game hashes)`);
|
|
506
|
+
return { deletedKeys: allKeys.length };
|
|
507
|
+
} catch (error) {
|
|
508
|
+
console.error('[GamesCache] Error flushing all caches:', error.message);
|
|
509
|
+
throw error;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Remove orphaned game IDs from the sorted set (IDs whose hash data is missing).
|
|
515
|
+
* Called in the background when inconsistency is detected during reads.
|
|
516
|
+
*/
|
|
517
|
+
async _cleanupOrphanedEntries(walletAddress, orphanedGameIds) {
|
|
518
|
+
if (!this.isEnabled() || orphanedGameIds.length === 0) return;
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
const pipeline = redisService.pipeline();
|
|
522
|
+
if (!pipeline) return;
|
|
523
|
+
|
|
524
|
+
for (const gameId of orphanedGameIds) {
|
|
525
|
+
pipeline.zrem(KEYS.USER_GAMES(walletAddress), gameId);
|
|
526
|
+
}
|
|
527
|
+
await pipeline.exec();
|
|
528
|
+
|
|
529
|
+
console.log(`[GamesCache] Cleaned up ${orphanedGameIds.length} orphaned entries for ${walletAddress.slice(0, 8)}...`);
|
|
530
|
+
} catch (error) {
|
|
531
|
+
console.error('[GamesCache] Error cleaning orphaned entries:', error.message);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Singleton instance
|
|
537
|
+
const gamesCacheService = new GamesCacheService();
|
|
538
|
+
|
|
539
|
+
module.exports = gamesCacheService;
|