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,881 @@
|
|
|
1
|
+
# ⚡ Real-Time WebSocket Architecture
|
|
2
|
+
|
|
3
|
+
**Goal:** Instant updates across all connected users
|
|
4
|
+
**Use Case:** Player joins in Japan → Users in USA see it within 100ms
|
|
5
|
+
**Status:** Design Complete, Ready to Implement
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🎯 Why WebSockets are CRITICAL
|
|
10
|
+
|
|
11
|
+
### **Polling (Current) - NOT GOOD ENOUGH:**
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
User A joins → Transaction confirms → 0-2 second delay → User B sees it
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Problems:**
|
|
18
|
+
- Feels sluggish
|
|
19
|
+
- Not truly "live"
|
|
20
|
+
- Wastes API calls
|
|
21
|
+
- Battery drain on mobile
|
|
22
|
+
|
|
23
|
+
### **WebSockets - SMOOTH:**
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
User A joins → Transaction confirms → <100ms → User B sees it instantly
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Benefits:**
|
|
30
|
+
- ✅ Feels instant and live
|
|
31
|
+
- ✅ True multiplayer experience
|
|
32
|
+
- ✅ Lower server load
|
|
33
|
+
- ✅ Battery friendly
|
|
34
|
+
- ✅ Can push keeper events in real-time
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 🏗️ Architecture Design
|
|
39
|
+
|
|
40
|
+
### **Components:**
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
44
|
+
│ WEBSOCKET FLOW │
|
|
45
|
+
└─────────────────────────────────────────────────────────────┘
|
|
46
|
+
|
|
47
|
+
1. KEEPER BOT (Server)
|
|
48
|
+
├─ Processes round (lock/reveal/resolve)
|
|
49
|
+
├─ Emits events to WebSocket server
|
|
50
|
+
└─ "player_joined", "round_locked", "winner_selected"
|
|
51
|
+
|
|
52
|
+
2. WEBSOCKET SERVER (dubs-server)
|
|
53
|
+
├─ Maintains connections to all users
|
|
54
|
+
├─ Receives events from keeper
|
|
55
|
+
├─ Broadcasts to all connected clients
|
|
56
|
+
└─ Handles rooms (one per round)
|
|
57
|
+
|
|
58
|
+
3. CAROUSEL (Frontend)
|
|
59
|
+
├─ Connects to WebSocket
|
|
60
|
+
├─ Listens for events
|
|
61
|
+
├─ Updates UI instantly
|
|
62
|
+
└─ Falls back to polling if disconnected
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 🔌 Implementation Plan
|
|
68
|
+
|
|
69
|
+
### **Step 1: Add Socket.io to Server (30 min)**
|
|
70
|
+
|
|
71
|
+
**Install:**
|
|
72
|
+
```bash
|
|
73
|
+
cd dubs-server
|
|
74
|
+
npm install socket.io
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Update `server.js`:**
|
|
78
|
+
```javascript
|
|
79
|
+
const express = require('express');
|
|
80
|
+
const http = require('http');
|
|
81
|
+
const { Server } = require('socket.io');
|
|
82
|
+
|
|
83
|
+
const app = express();
|
|
84
|
+
const server = http.createServer(app);
|
|
85
|
+
|
|
86
|
+
// Socket.io with CORS
|
|
87
|
+
const io = new Server(server, {
|
|
88
|
+
cors: {
|
|
89
|
+
origin: [
|
|
90
|
+
'http://localhost:3000',
|
|
91
|
+
'https://your-netlify-app.netlify.app'
|
|
92
|
+
],
|
|
93
|
+
methods: ['GET', 'POST']
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// WebSocket connection handler
|
|
98
|
+
io.on('connection', (socket) => {
|
|
99
|
+
console.log('🔌 Client connected:', socket.id);
|
|
100
|
+
|
|
101
|
+
// Join current round room
|
|
102
|
+
socket.on('join_round', (roundId) => {
|
|
103
|
+
socket.join(`round_${roundId}`);
|
|
104
|
+
console.log(`👥 Client ${socket.id} joined round ${roundId}`);
|
|
105
|
+
|
|
106
|
+
// Send current state immediately
|
|
107
|
+
getCurrentRoundState(roundId).then(state => {
|
|
108
|
+
socket.emit('round_state', state);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
socket.on('disconnect', () => {
|
|
113
|
+
console.log('🔌 Client disconnected:', socket.id);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Change app.listen to server.listen
|
|
118
|
+
server.listen(PORT, () => {
|
|
119
|
+
console.log(`🚀 Server running on port ${PORT}`);
|
|
120
|
+
console.log(`⚡ WebSocket ready`);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Make io available globally for emitting from keeper
|
|
124
|
+
global.io = io;
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### **Step 2: Emit Events from Keeper (15 min)**
|
|
130
|
+
|
|
131
|
+
**Update `jackpot-keeper.js`:**
|
|
132
|
+
|
|
133
|
+
```javascript
|
|
134
|
+
class JackpotKeeper {
|
|
135
|
+
constructor() {
|
|
136
|
+
// ... existing code ...
|
|
137
|
+
this.io = null; // Will be set via setter
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
setSocketServer(io) {
|
|
141
|
+
this.io = io;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async lockRound(roundId) {
|
|
145
|
+
// ... existing lock logic ...
|
|
146
|
+
|
|
147
|
+
// Emit event
|
|
148
|
+
if (this.io) {
|
|
149
|
+
this.io.to(`round_${roundId}`).emit('round_locked', {
|
|
150
|
+
roundId,
|
|
151
|
+
timestamp: Date.now()
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async resolveRound(roundId) {
|
|
159
|
+
// ... existing resolve logic ...
|
|
160
|
+
|
|
161
|
+
const winner = data.winner;
|
|
162
|
+
const winAmount = Number(totalPot) * 0.95;
|
|
163
|
+
|
|
164
|
+
// Emit winner event
|
|
165
|
+
if (this.io) {
|
|
166
|
+
this.io.to(`round_${roundId}`).emit('winner_selected', {
|
|
167
|
+
roundId,
|
|
168
|
+
winner,
|
|
169
|
+
winAmount,
|
|
170
|
+
timestamp: Date.now()
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async openOrResetRound(previousRoundId) {
|
|
178
|
+
// ... existing reset logic ...
|
|
179
|
+
|
|
180
|
+
const newRoundId = data.roundId;
|
|
181
|
+
|
|
182
|
+
// Emit new round event
|
|
183
|
+
if (this.io) {
|
|
184
|
+
// Broadcast to all clients
|
|
185
|
+
this.io.emit('round_opened', {
|
|
186
|
+
roundId: newRoundId,
|
|
187
|
+
timestamp: Date.now()
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Export keeper instance
|
|
196
|
+
module.exports = JackpotKeeper;
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**In keeper startup script:**
|
|
200
|
+
```javascript
|
|
201
|
+
// server.js or separate keeper init
|
|
202
|
+
const keeper = new JackpotKeeper();
|
|
203
|
+
keeper.setSocketServer(io);
|
|
204
|
+
keeper.run();
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
### **Step 3: Listen for New Players (Real-Time)**
|
|
210
|
+
|
|
211
|
+
**Add endpoint to emit when player joins:**
|
|
212
|
+
|
|
213
|
+
**Update `jackpotRoutes.js`:**
|
|
214
|
+
```javascript
|
|
215
|
+
router.post('/enter', async (req, res) => {
|
|
216
|
+
try {
|
|
217
|
+
// ... existing enter logic ...
|
|
218
|
+
|
|
219
|
+
const result = await jackpotService.enterRound(...);
|
|
220
|
+
|
|
221
|
+
// Emit to all users in this round
|
|
222
|
+
if (global.io) {
|
|
223
|
+
global.io.to(`round_${roundId}`).emit('player_joined', {
|
|
224
|
+
roundId,
|
|
225
|
+
player: playerAddress,
|
|
226
|
+
weight: amount,
|
|
227
|
+
entryCount: newEntryCount,
|
|
228
|
+
totalPot: newTotalPot,
|
|
229
|
+
timestamp: Date.now()
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
res.json(result);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
// ... error handling ...
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
### **Step 4: Frontend WebSocket Integration (30 min)**
|
|
243
|
+
|
|
244
|
+
**Install:**
|
|
245
|
+
```bash
|
|
246
|
+
cd dubs-jackpot
|
|
247
|
+
npm install socket.io-client
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Create WebSocket Hook:**
|
|
251
|
+
|
|
252
|
+
File: `dubs-jackpot/app/hooks/useJackpotSocket.ts`
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
import { useEffect, useState, useCallback } from 'react';
|
|
256
|
+
import { io, Socket } from 'socket.io-client';
|
|
257
|
+
|
|
258
|
+
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
|
|
259
|
+
|
|
260
|
+
export function useJackpotSocket(roundId: string | null) {
|
|
261
|
+
const [socket, setSocket] = useState<Socket | null>(null);
|
|
262
|
+
const [connected, setConnected] = useState(false);
|
|
263
|
+
const [players, setPlayers] = useState<any[]>([]);
|
|
264
|
+
const [roundState, setRoundState] = useState<any>(null);
|
|
265
|
+
|
|
266
|
+
// Connect to WebSocket
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
const newSocket = io(API_BASE, {
|
|
269
|
+
transports: ['websocket', 'polling'],
|
|
270
|
+
reconnection: true,
|
|
271
|
+
reconnectionDelay: 1000,
|
|
272
|
+
reconnectionAttempts: 5
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
newSocket.on('connect', () => {
|
|
276
|
+
console.log('⚡ WebSocket connected');
|
|
277
|
+
setConnected(true);
|
|
278
|
+
|
|
279
|
+
// Join current round room
|
|
280
|
+
if (roundId) {
|
|
281
|
+
newSocket.emit('join_round', roundId);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
newSocket.on('disconnect', () => {
|
|
286
|
+
console.log('🔌 WebSocket disconnected');
|
|
287
|
+
setConnected(false);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
setSocket(newSocket);
|
|
291
|
+
|
|
292
|
+
return () => {
|
|
293
|
+
newSocket.close();
|
|
294
|
+
};
|
|
295
|
+
}, []);
|
|
296
|
+
|
|
297
|
+
// Join round room when roundId changes
|
|
298
|
+
useEffect(() => {
|
|
299
|
+
if (socket && roundId) {
|
|
300
|
+
socket.emit('join_round', roundId);
|
|
301
|
+
}
|
|
302
|
+
}, [socket, roundId]);
|
|
303
|
+
|
|
304
|
+
// Listen for player joined
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
if (!socket) return;
|
|
307
|
+
|
|
308
|
+
const handlePlayerJoined = (data: any) => {
|
|
309
|
+
console.log('👤 Player joined:', data);
|
|
310
|
+
|
|
311
|
+
// Add new player to carousel
|
|
312
|
+
setPlayers(prev => {
|
|
313
|
+
// Check if already exists
|
|
314
|
+
if (prev.find(p => p.address === data.player)) {
|
|
315
|
+
return prev;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Add new player
|
|
319
|
+
return [...prev, {
|
|
320
|
+
address: data.player,
|
|
321
|
+
wager: data.weight / 1e9,
|
|
322
|
+
emoji: hashToEmoji(data.player),
|
|
323
|
+
username: data.player.slice(0, 4) + '...',
|
|
324
|
+
timestamp: data.timestamp
|
|
325
|
+
}];
|
|
326
|
+
});
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
socket.on('player_joined', handlePlayerJoined);
|
|
330
|
+
|
|
331
|
+
return () => {
|
|
332
|
+
socket.off('player_joined', handlePlayerJoined);
|
|
333
|
+
};
|
|
334
|
+
}, [socket]);
|
|
335
|
+
|
|
336
|
+
// Listen for round locked
|
|
337
|
+
useEffect(() => {
|
|
338
|
+
if (!socket) return;
|
|
339
|
+
|
|
340
|
+
const handleRoundLocked = (data: any) => {
|
|
341
|
+
console.log('🔒 Round locked:', data);
|
|
342
|
+
setRoundState({ status: 'locked', roundId: data.roundId });
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
socket.on('round_locked', handleRoundLocked);
|
|
346
|
+
|
|
347
|
+
return () => {
|
|
348
|
+
socket.off('round_locked', handleRoundLocked);
|
|
349
|
+
};
|
|
350
|
+
}, [socket]);
|
|
351
|
+
|
|
352
|
+
// Listen for winner selected
|
|
353
|
+
useEffect(() => {
|
|
354
|
+
if (!socket) return;
|
|
355
|
+
|
|
356
|
+
const handleWinnerSelected = (data: any) => {
|
|
357
|
+
console.log('🏆 Winner selected:', data);
|
|
358
|
+
|
|
359
|
+
setRoundState({
|
|
360
|
+
status: 'resolved',
|
|
361
|
+
winner: data.winner,
|
|
362
|
+
winAmount: data.winAmount,
|
|
363
|
+
roundId: data.roundId
|
|
364
|
+
});
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
socket.on('winner_selected', handleWinnerSelected);
|
|
368
|
+
|
|
369
|
+
return () => {
|
|
370
|
+
socket.off('winner_selected', handleWinnerSelected);
|
|
371
|
+
};
|
|
372
|
+
}, [socket]);
|
|
373
|
+
|
|
374
|
+
// Listen for new round
|
|
375
|
+
useEffect(() => {
|
|
376
|
+
if (!socket) return;
|
|
377
|
+
|
|
378
|
+
const handleRoundOpened = (data: any) => {
|
|
379
|
+
console.log('🎰 New round opened:', data);
|
|
380
|
+
|
|
381
|
+
setPlayers([]); // Clear players
|
|
382
|
+
setRoundState({
|
|
383
|
+
status: 'open',
|
|
384
|
+
roundId: data.roundId
|
|
385
|
+
});
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
socket.on('round_opened', handleRoundOpened);
|
|
389
|
+
|
|
390
|
+
return () => {
|
|
391
|
+
socket.off('round_opened', handleRoundOpened);
|
|
392
|
+
};
|
|
393
|
+
}, [socket]);
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
connected,
|
|
397
|
+
players,
|
|
398
|
+
roundState,
|
|
399
|
+
socket
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Helper
|
|
404
|
+
function hashToEmoji(address: string): string {
|
|
405
|
+
const EMOJIS = ['🔥', '⚡', '🌊', '🌿', '🎮', '⭐', '💎', '🎯', '🚀', '🌟'];
|
|
406
|
+
const hash = address.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0);
|
|
407
|
+
return EMOJIS[hash % EMOJIS.length];
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
### **Step 5: Use in Carousel (10 min)**
|
|
414
|
+
|
|
415
|
+
**Update `JackpotCarousel.tsx`:**
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
export default function JackpotCarousel() {
|
|
419
|
+
const [currentRound, setCurrentRound] = useState<any>(null);
|
|
420
|
+
|
|
421
|
+
// Get current round ID
|
|
422
|
+
useEffect(() => {
|
|
423
|
+
fetch(`${API_BASE}/jackpot/round/current`)
|
|
424
|
+
.then(r => r.json())
|
|
425
|
+
.then(data => setCurrentRound(data.round));
|
|
426
|
+
}, []);
|
|
427
|
+
|
|
428
|
+
// Use WebSocket hook
|
|
429
|
+
const { connected, players, roundState } = useJackpotSocket(
|
|
430
|
+
currentRound?.roundId
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
// Trigger winner animation when winner selected
|
|
434
|
+
useEffect(() => {
|
|
435
|
+
if (roundState?.status === 'resolved' && roundState.winner) {
|
|
436
|
+
const winnerPlayer = players.find(p => p.address === roundState.winner);
|
|
437
|
+
|
|
438
|
+
if (winnerPlayer) {
|
|
439
|
+
// Trigger carousel animation
|
|
440
|
+
triggerWinnerAnimation(winnerPlayer, roundState.winAmount);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}, [roundState]);
|
|
444
|
+
|
|
445
|
+
return (
|
|
446
|
+
<div>
|
|
447
|
+
{/* Connection indicator */}
|
|
448
|
+
<div className="connection-status">
|
|
449
|
+
{connected ? '🟢 Live' : '🔴 Reconnecting...'}
|
|
450
|
+
</div>
|
|
451
|
+
|
|
452
|
+
<InfiniteCarousel
|
|
453
|
+
players={players}
|
|
454
|
+
roundState={roundState}
|
|
455
|
+
/>
|
|
456
|
+
</div>
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
## ⚡ Event Types
|
|
464
|
+
|
|
465
|
+
### **Server → Client Events:**
|
|
466
|
+
|
|
467
|
+
1. **`player_joined`**
|
|
468
|
+
```json
|
|
469
|
+
{
|
|
470
|
+
"roundId": "556",
|
|
471
|
+
"player": "Abc...xyz",
|
|
472
|
+
"weight": "50000000",
|
|
473
|
+
"entryCount": 5,
|
|
474
|
+
"totalPot": "250000000",
|
|
475
|
+
"timestamp": 1700000000
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
**UI Action:** Add card to carousel (RIGHT-TO-LEFT animation)
|
|
479
|
+
|
|
480
|
+
2. **`round_locked`**
|
|
481
|
+
```json
|
|
482
|
+
{
|
|
483
|
+
"roundId": "556",
|
|
484
|
+
"timestamp": 1700000000
|
|
485
|
+
}
|
|
486
|
+
```
|
|
487
|
+
**UI Action:** Show "Picking winner...", speed up carousel
|
|
488
|
+
|
|
489
|
+
3. **`winner_selected`**
|
|
490
|
+
```json
|
|
491
|
+
{
|
|
492
|
+
"roundId": "556",
|
|
493
|
+
"winner": "Abc...xyz",
|
|
494
|
+
"winAmount": "237500000",
|
|
495
|
+
"timestamp": 1700000000
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
**UI Action:** Scroll to winner, show badge
|
|
499
|
+
|
|
500
|
+
4. **`round_opened`**
|
|
501
|
+
```json
|
|
502
|
+
{
|
|
503
|
+
"roundId": "557",
|
|
504
|
+
"timestamp": 1700000000
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
**UI Action:** Reset carousel, clear players
|
|
508
|
+
|
|
509
|
+
5. **`timer_update`** (every 5 seconds)
|
|
510
|
+
```json
|
|
511
|
+
{
|
|
512
|
+
"roundId": "556",
|
|
513
|
+
"timeRemaining": 45,
|
|
514
|
+
"status": "open"
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
**UI Action:** Update countdown
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
521
|
+
## 🎯 Implementation Strategy
|
|
522
|
+
|
|
523
|
+
### **Hybrid Approach (Best of Both Worlds):**
|
|
524
|
+
|
|
525
|
+
**WebSockets for:**
|
|
526
|
+
- ✅ Player joins (instant!)
|
|
527
|
+
- ✅ Round state changes (locked, resolved)
|
|
528
|
+
- ✅ Winner announcements
|
|
529
|
+
- ✅ New round notifications
|
|
530
|
+
|
|
531
|
+
**Polling for:**
|
|
532
|
+
- ✅ Initial load (fetch current state)
|
|
533
|
+
- ✅ Fallback if WebSocket fails
|
|
534
|
+
- ✅ Every 10s to verify sync (catch missed events)
|
|
535
|
+
|
|
536
|
+
**Code:**
|
|
537
|
+
```typescript
|
|
538
|
+
export function useJackpotData(roundId: string) {
|
|
539
|
+
const socket = useJackpotSocket(roundId);
|
|
540
|
+
const [players, setPlayers] = useState([]);
|
|
541
|
+
|
|
542
|
+
// WebSocket updates (instant)
|
|
543
|
+
useEffect(() => {
|
|
544
|
+
if (socket.players.length > 0) {
|
|
545
|
+
setPlayers(socket.players);
|
|
546
|
+
}
|
|
547
|
+
}, [socket.players]);
|
|
548
|
+
|
|
549
|
+
// Polling fallback (every 10s to verify)
|
|
550
|
+
useEffect(() => {
|
|
551
|
+
const verify = async () => {
|
|
552
|
+
if (!socket.connected) {
|
|
553
|
+
// WebSocket down, use polling
|
|
554
|
+
const data = await fetchEntries(roundId);
|
|
555
|
+
setPlayers(data);
|
|
556
|
+
} else {
|
|
557
|
+
// WebSocket working, just verify sync
|
|
558
|
+
const data = await fetchEntries(roundId);
|
|
559
|
+
if (data.length !== players.length) {
|
|
560
|
+
console.warn('⚠️ Socket/API mismatch, resync');
|
|
561
|
+
setPlayers(data);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const interval = setInterval(verify, 10000);
|
|
567
|
+
return () => clearInterval(interval);
|
|
568
|
+
}, [roundId, socket.connected, players.length]);
|
|
569
|
+
|
|
570
|
+
return {
|
|
571
|
+
players,
|
|
572
|
+
connected: socket.connected,
|
|
573
|
+
roundState: socket.roundState
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
---
|
|
579
|
+
|
|
580
|
+
## 🔧 Server-Side Event Emission
|
|
581
|
+
|
|
582
|
+
### **Where to Emit:**
|
|
583
|
+
|
|
584
|
+
**1. When player enters (API endpoint):**
|
|
585
|
+
```javascript
|
|
586
|
+
// routes/jackpotRoutes.js
|
|
587
|
+
router.post('/enter', async (req, res) => {
|
|
588
|
+
// ... process entry ...
|
|
589
|
+
|
|
590
|
+
// Emit to all users watching this round
|
|
591
|
+
global.io.to(`round_${roundId}`).emit('player_joined', {
|
|
592
|
+
roundId,
|
|
593
|
+
player: playerAddress,
|
|
594
|
+
weight: amount,
|
|
595
|
+
entryCount: round.entryCount,
|
|
596
|
+
totalPot: round.totalPotLamports
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
**2. In keeper bot:**
|
|
602
|
+
```javascript
|
|
603
|
+
// scripts/jackpot-keeper.js
|
|
604
|
+
|
|
605
|
+
async lockRound(roundId) {
|
|
606
|
+
// ... lock logic ...
|
|
607
|
+
|
|
608
|
+
// Broadcast to all users
|
|
609
|
+
this.io.to(`round_${roundId}`).emit('round_locked', { roundId });
|
|
610
|
+
this.io.emit('global_event', { type: 'round_locked', roundId });
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async resolveRound(roundId) {
|
|
614
|
+
// ... resolve logic ...
|
|
615
|
+
|
|
616
|
+
// Broadcast winner
|
|
617
|
+
this.io.to(`round_${roundId}`).emit('winner_selected', {
|
|
618
|
+
roundId,
|
|
619
|
+
winner,
|
|
620
|
+
winAmount
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
**3. Timer updates (optional, every 5s):**
|
|
626
|
+
```javascript
|
|
627
|
+
setInterval(() => {
|
|
628
|
+
getCurrentRound().then(round => {
|
|
629
|
+
if (round) {
|
|
630
|
+
io.to(`round_${round.roundId}`).emit('timer_update', {
|
|
631
|
+
roundId: round.roundId,
|
|
632
|
+
timeRemaining: round.timeRemainingSlots * 0.4,
|
|
633
|
+
totalPot: round.totalPotLamports,
|
|
634
|
+
entryCount: round.entryCount
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
}, 5000);
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
---
|
|
642
|
+
|
|
643
|
+
## 🎨 Carousel Integration
|
|
644
|
+
|
|
645
|
+
### **Updated InfiniteCarousel:**
|
|
646
|
+
|
|
647
|
+
```typescript
|
|
648
|
+
export default function InfiniteCarousel({
|
|
649
|
+
roundId
|
|
650
|
+
}: { roundId: string | null }) {
|
|
651
|
+
const INITIAL_SLOTS = 7;
|
|
652
|
+
const [players, setPlayers] = useState<(Player | null)[]>(Array(INITIAL_SLOTS).fill(null));
|
|
653
|
+
|
|
654
|
+
// WebSocket hook
|
|
655
|
+
const { connected, players: realPlayers, roundState } = useJackpotSocket(roundId);
|
|
656
|
+
|
|
657
|
+
// Sync real players to carousel
|
|
658
|
+
useEffect(() => {
|
|
659
|
+
if (realPlayers.length > 0) {
|
|
660
|
+
// Map to carousel format
|
|
661
|
+
const carouselPlayers = realPlayers.map(p => ({
|
|
662
|
+
id: p.address,
|
|
663
|
+
username: p.username,
|
|
664
|
+
emoji: p.emoji,
|
|
665
|
+
wager: p.wager,
|
|
666
|
+
address: p.address
|
|
667
|
+
}));
|
|
668
|
+
|
|
669
|
+
// Fill slots right-to-left
|
|
670
|
+
const slots = Array(Math.max(INITIAL_SLOTS, carouselPlayers.length)).fill(null);
|
|
671
|
+
carouselPlayers.forEach((player, i) => {
|
|
672
|
+
slots[slots.length - 1 - i] = player; // Right-to-left
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
setPlayers(slots);
|
|
676
|
+
}
|
|
677
|
+
}, [realPlayers]);
|
|
678
|
+
|
|
679
|
+
// React to round state changes
|
|
680
|
+
useEffect(() => {
|
|
681
|
+
if (roundState?.status === 'locked') {
|
|
682
|
+
// Speed up carousel, show "Picking winner..."
|
|
683
|
+
setIsPickingWinner(true);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (roundState?.status === 'resolved' && roundState.winner) {
|
|
687
|
+
// Trigger winner animation
|
|
688
|
+
const winnerPlayer = players.find(p => p?.address === roundState.winner);
|
|
689
|
+
if (winnerPlayer) {
|
|
690
|
+
handlePickWinner(winnerPlayer);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (roundState?.status === 'open' && roundState.roundId !== currentRound) {
|
|
695
|
+
// New round started
|
|
696
|
+
handleReset();
|
|
697
|
+
}
|
|
698
|
+
}, [roundState]);
|
|
699
|
+
|
|
700
|
+
return (
|
|
701
|
+
<>
|
|
702
|
+
{/* Connection indicator */}
|
|
703
|
+
<div className={`status ${connected ? 'connected' : 'disconnected'}`}>
|
|
704
|
+
{connected ? '⚡ LIVE' : '🔄 Reconnecting...'}
|
|
705
|
+
</div>
|
|
706
|
+
|
|
707
|
+
{/* Carousel */}
|
|
708
|
+
{/* ... rest of carousel UI ... */}
|
|
709
|
+
</>
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
---
|
|
715
|
+
|
|
716
|
+
## 🚀 Performance Optimization
|
|
717
|
+
|
|
718
|
+
### **1. Debounce Rapid Events:**
|
|
719
|
+
|
|
720
|
+
```typescript
|
|
721
|
+
// If 5 players join in 1 second, batch them
|
|
722
|
+
const [playerQueue, setPlayerQueue] = useState([]);
|
|
723
|
+
|
|
724
|
+
useEffect(() => {
|
|
725
|
+
socket.on('player_joined', (player) => {
|
|
726
|
+
setPlayerQueue(prev => [...prev, player]);
|
|
727
|
+
});
|
|
728
|
+
}, [socket]);
|
|
729
|
+
|
|
730
|
+
useEffect(() => {
|
|
731
|
+
if (playerQueue.length > 0) {
|
|
732
|
+
const timeout = setTimeout(() => {
|
|
733
|
+
// Add all queued players at once
|
|
734
|
+
setPlayers(prev => [...prev, ...playerQueue]);
|
|
735
|
+
setPlayerQueue([]);
|
|
736
|
+
}, 300); // 300ms debounce
|
|
737
|
+
|
|
738
|
+
return () => clearTimeout(timeout);
|
|
739
|
+
}
|
|
740
|
+
}, [playerQueue]);
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
### **2. Efficient Re-renders:**
|
|
744
|
+
|
|
745
|
+
```typescript
|
|
746
|
+
// Memoize players to prevent unnecessary renders
|
|
747
|
+
const memoizedPlayers = useMemo(() => players, [players.length, players[0]?.id]);
|
|
748
|
+
|
|
749
|
+
// Only re-render carousel when players actually change
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
### **3. Background Sync:**
|
|
753
|
+
|
|
754
|
+
```typescript
|
|
755
|
+
// Verify socket data matches blockchain every 30s
|
|
756
|
+
useEffect(() => {
|
|
757
|
+
const verify = async () => {
|
|
758
|
+
const canonical = await fetchEntries(roundId);
|
|
759
|
+
if (canonical.length !== players.length) {
|
|
760
|
+
console.warn('Resync needed');
|
|
761
|
+
setPlayers(canonical);
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
const interval = setInterval(verify, 30000);
|
|
766
|
+
return () => clearInterval(interval);
|
|
767
|
+
}, [roundId]);
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
---
|
|
771
|
+
|
|
772
|
+
## 🎯 Why This Will Be SMOOTH
|
|
773
|
+
|
|
774
|
+
**Player joins in Tokyo:**
|
|
775
|
+
```
|
|
776
|
+
1. Transaction confirms (2-3s)
|
|
777
|
+
2. Backend emits player_joined (instant)
|
|
778
|
+
3. WebSocket pushes to all clients (<100ms)
|
|
779
|
+
4. Carousel in NYC adds card (instant)
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
**Total delay: <100ms after blockchain confirmation!**
|
|
783
|
+
|
|
784
|
+
**vs Polling:**
|
|
785
|
+
```
|
|
786
|
+
1. Transaction confirms (2-3s)
|
|
787
|
+
2. User in NYC polls API (0-2s wait)
|
|
788
|
+
3. Carousel updates
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
**Total delay: 0-2 seconds** 😢
|
|
792
|
+
|
|
793
|
+
---
|
|
794
|
+
|
|
795
|
+
## 📋 Implementation Checklist
|
|
796
|
+
|
|
797
|
+
**Backend:**
|
|
798
|
+
- [ ] Install socket.io
|
|
799
|
+
- [ ] Add WebSocket server to server.js
|
|
800
|
+
- [ ] Emit events from keeper
|
|
801
|
+
- [ ] Emit events from API endpoints
|
|
802
|
+
- [ ] Test events with socket.io-client
|
|
803
|
+
|
|
804
|
+
**Frontend:**
|
|
805
|
+
- [ ] Install socket.io-client
|
|
806
|
+
- [ ] Create useJackpotSocket hook
|
|
807
|
+
- [ ] Integrate with carousel
|
|
808
|
+
- [ ] Add connection indicator
|
|
809
|
+
- [ ] Add fallback polling
|
|
810
|
+
- [ ] Test real-time updates
|
|
811
|
+
|
|
812
|
+
**Testing:**
|
|
813
|
+
- [ ] 2 users, different browsers
|
|
814
|
+
- [ ] User A joins → User B sees instantly
|
|
815
|
+
- [ ] Winner selected → Both see animation
|
|
816
|
+
- [ ] Network disconnect → Falls back to polling
|
|
817
|
+
- [ ] Reconnect → Resync automatically
|
|
818
|
+
|
|
819
|
+
---
|
|
820
|
+
|
|
821
|
+
## 🎯 Estimated Timeline
|
|
822
|
+
|
|
823
|
+
**Day 1 - Backend WebSocket (3 hours):**
|
|
824
|
+
- Hour 1: Add Socket.io to server
|
|
825
|
+
- Hour 2: Emit events from keeper
|
|
826
|
+
- Hour 3: Test with socket.io-client CLI
|
|
827
|
+
|
|
828
|
+
**Day 2 - Frontend Integration (4 hours):**
|
|
829
|
+
- Hour 1: Create useJackpotSocket hook
|
|
830
|
+
- Hour 2: Integrate with carousel
|
|
831
|
+
- Hour 3: Add fallback & error handling
|
|
832
|
+
- Hour 4: Test with real rounds
|
|
833
|
+
|
|
834
|
+
**Day 3 - Polish & Deploy (2 hours):**
|
|
835
|
+
- Hour 1: Connection indicators, loading states
|
|
836
|
+
- Hour 2: Deploy & monitor
|
|
837
|
+
|
|
838
|
+
**Total: ~9 hours** for real-time multiplayer jackpot! 🚀
|
|
839
|
+
|
|
840
|
+
---
|
|
841
|
+
|
|
842
|
+
## 💎 The Result
|
|
843
|
+
|
|
844
|
+
**Users will see:**
|
|
845
|
+
|
|
846
|
+
```
|
|
847
|
+
[You join from USA]
|
|
848
|
+
→ Card appears instantly on your screen
|
|
849
|
+
→ Card appears instantly on Tokyo user's screen (<100ms)
|
|
850
|
+
|
|
851
|
+
[Tokyo user joins]
|
|
852
|
+
→ Card whooshes in from right on your screen (<100ms)
|
|
853
|
+
→ Feels ALIVE and multiplayer
|
|
854
|
+
|
|
855
|
+
[Round locks]
|
|
856
|
+
→ Everyone sees carousel speed up (synchronized)
|
|
857
|
+
|
|
858
|
+
[Winner selected]
|
|
859
|
+
→ Everyone sees same smooth scroll-to-winner
|
|
860
|
+
→ Same winner badge animation
|
|
861
|
+
→ True shared experience
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
**This is what makes it feel like a REAL multiplayer game!** 🎮
|
|
865
|
+
|
|
866
|
+
---
|
|
867
|
+
|
|
868
|
+
## 🎯 My Recommendation
|
|
869
|
+
|
|
870
|
+
**YES, absolutely use WebSockets!**
|
|
871
|
+
|
|
872
|
+
**Why:**
|
|
873
|
+
1. ✅ Minimal extra effort (~3 hours backend, ~4 hours frontend)
|
|
874
|
+
2. ✅ Massive UX improvement (feels alive!)
|
|
875
|
+
3. ✅ Required for true multiplayer feel
|
|
876
|
+
4. ✅ Socket.io handles all edge cases (reconnection, fallback)
|
|
877
|
+
5. ✅ Can scale to 100s of concurrent users
|
|
878
|
+
|
|
879
|
+
**With your smooth carousel + real-time WebSockets = Magic!** ✨
|
|
880
|
+
|
|
881
|
+
Want me to start implementing the WebSocket system? I can have the backend ready in ~1 hour!
|