-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Expand file tree
/
Copy pathindex.ts
More file actions
146 lines (119 loc) · 4.81 KB
/
Copy pathindex.ts
File metadata and controls
146 lines (119 loc) · 4.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import { createServer } from 'http'
import { createLogger } from '@sim/logger'
import type { Server as SocketIOServer } from 'socket.io'
import { startAccessRevalidationSweep } from '@/access-revalidation'
import { createSocketIOServer, shutdownSocketIOAdapter } from '@/config/socket'
import { assertSchemaCompatibility } from '@/database/preflight'
import { env } from '@/env'
import { setupAllHandlers } from '@/handlers'
import { type AuthenticatedSocket, authenticateSocket } from '@/middleware/auth'
import { type IRoomManager, MemoryRoomManager, RedisRoomManager } from '@/rooms'
import { createHttpHandler } from '@/routes/http'
const logger = createLogger('CollaborativeSocketServer')
/** Maximum time to wait for graceful shutdown before forcing exit */
const SHUTDOWN_TIMEOUT_MS = 10000
async function createRoomManager(io: SocketIOServer): Promise<IRoomManager> {
if (env.REDIS_URL) {
logger.info('Initializing Redis-backed RoomManager for multi-pod support')
const manager = new RedisRoomManager(io, env.REDIS_URL)
await manager.initialize()
return manager
}
logger.warn('No REDIS_URL configured - using in-memory RoomManager (single-pod only)')
const manager = new MemoryRoomManager(io)
await manager.initialize()
return manager
}
async function main() {
const httpServer = createServer()
const PORT = env.PORT
logger.info('Starting Socket.IO server...', {
port: PORT,
nodeEnv: env.NODE_ENV,
hasDatabase: !!env.DATABASE_URL,
hasAuth: !!env.BETTER_AUTH_SECRET,
hasRedis: !!env.REDIS_URL,
})
// Register the HTTP handler before Socket.IO attaches: engine.io captures
// pre-existing `request` listeners and forwards only non-`/socket.io/`
// requests to them, making it the single dispatcher for the shared port.
// The handler itself is assigned after the room manager exists, before listen().
// biome-ignore lint/style/useConst: must be declared before the request listener closure; assigned only after the room manager exists
let httpHandler: ReturnType<typeof createHttpHandler> | undefined
httpServer.on('request', (req, res) => httpHandler?.(req, res))
// Create Socket.IO server with Redis adapter if configured
const io = await createSocketIOServer(httpServer)
// Initialize room manager (Redis or in-memory based on config)
const roomManager = await createRoomManager(io)
// Set up authentication middleware
io.use(authenticateSocket)
// Set up HTTP handler for health checks and internal APIs
httpHandler = createHttpHandler(roomManager, logger)
// Global error handlers
process.on('uncaughtException', (error) => {
logger.error('Uncaught Exception:', error)
})
process.on('unhandledRejection', (reason, promise) => {
if (reason instanceof Error && reason.message === 'The client is closed') {
logger.warn('Redis client is closed — suppressing unhandled rejection')
return
}
logger.error('Unhandled Rejection at:', promise, 'reason:', reason)
})
httpServer.on('error', (error: NodeJS.ErrnoException) => {
logger.error('HTTP server error:', error)
if (error.code === 'EADDRINUSE' || error.code === 'EACCES') {
process.exit(1)
}
})
io.engine.on('connection_error', (err) => {
logger.error('Socket.IO connection error:', {
req: err.req?.url,
code: err.code,
message: err.message,
context: err.context,
})
})
io.on('connection', (socket: AuthenticatedSocket) => {
logger.info(`New socket connection: ${socket.id}`)
setupAllHandlers(socket, roomManager)
})
// Bound read-access staleness: periodically re-validate connected sockets and
// evict any whose workspace permission has been revoked, matching the write path.
const accessRevalidation = startAccessRevalidationSweep(roomManager)
await assertSchemaCompatibility()
httpServer.listen(PORT, '0.0.0.0', () => {
logger.info(`Socket.IO server running on port ${PORT}`)
logger.info(`Health check available at: http://localhost:${PORT}/health`)
})
const shutdown = async () => {
logger.info('Shutting down Socket.IO server...')
accessRevalidation.stop()
try {
await roomManager.shutdown()
logger.info('RoomManager shutdown complete')
} catch (error) {
logger.error('Error during RoomManager shutdown:', error)
}
try {
await shutdownSocketIOAdapter()
} catch (error) {
logger.error('Error during Socket.IO adapter shutdown:', error)
}
httpServer.close(() => {
logger.info('Socket.IO server closed')
process.exit(0)
})
setTimeout(() => {
logger.error('Forced shutdown after timeout')
process.exit(1)
}, SHUTDOWN_TIMEOUT_MS)
}
process.on('SIGINT', shutdown)
process.on('SIGTERM', shutdown)
}
// Start the server
main().catch((error) => {
logger.error('Failed to start server:', error)
process.exit(1)
})