-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Expand file tree
/
Copy pathsocket.ts
More file actions
151 lines (127 loc) · 4.4 KB
/
socket.ts
File metadata and controls
151 lines (127 loc) · 4.4 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
147
148
149
150
151
import type { Server as HttpServer } from 'http'
import { createLogger } from '@sim/logger'
import { createAdapter } from '@socket.io/redis-adapter'
import { createClient, type RedisClientType } from 'redis'
import { Server } from 'socket.io'
import { env, getBaseUrl, isProd } from '@/env'
const logger = createLogger('SocketIOConfig')
/** Socket.IO ping timeout - how long to wait for pong before considering connection dead */
const PING_TIMEOUT_MS = 60000
/** Socket.IO ping interval - how often to send ping packets */
const PING_INTERVAL_MS = 25000
/** Maximum HTTP buffer size for Socket.IO messages */
const MAX_HTTP_BUFFER_SIZE = 1e6
let adapterPubClient: RedisClientType | null = null
let adapterSubClient: RedisClientType | null = null
function getAllowedOrigins(): string[] {
const allowedOrigins = [
getBaseUrl(),
'http://localhost:3000',
'http://localhost:3001',
...(env.ALLOWED_ORIGINS?.split(',') || []),
].filter((url): url is string => Boolean(url))
logger.info('Socket.IO CORS configuration:', { allowedOrigins })
return allowedOrigins
}
/**
* Create and configure a Socket.IO server instance.
* If REDIS_URL is configured, adds Redis adapter for cross-pod broadcasting.
*/
export async function createSocketIOServer(httpServer: HttpServer): Promise<Server> {
const allowedOrigins = getAllowedOrigins()
const io = new Server(httpServer, {
cors: {
origin: allowedOrigins,
methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'Cookie', 'socket.io'],
credentials: true,
},
transports: ['websocket', 'polling'],
allowEIO3: true,
pingTimeout: PING_TIMEOUT_MS,
pingInterval: PING_INTERVAL_MS,
maxHttpBufferSize: MAX_HTTP_BUFFER_SIZE,
cookie: {
name: 'io',
path: '/',
httpOnly: true,
sameSite: 'none',
secure: isProd,
},
})
if (env.REDIS_URL) {
logger.info('Configuring Socket.IO Redis adapter...')
const redisOptions = {
url: env.REDIS_URL,
socket: {
reconnectStrategy: (retries: number) => {
if (retries > 10) {
logger.error('Redis adapter reconnection failed after 10 attempts')
return new Error('Redis adapter reconnection failed')
}
const delay = Math.min(retries * 100, 3000)
logger.warn(`Redis adapter reconnecting in ${delay}ms (attempt ${retries})`)
return delay
},
},
}
// Create separate clients for pub and sub (recommended for reliability)
adapterPubClient = createClient(redisOptions)
adapterSubClient = createClient(redisOptions)
adapterPubClient.on('error', (err) => {
logger.error('Redis adapter pub client error:', err)
})
adapterSubClient.on('error', (err) => {
logger.error('Redis adapter sub client error:', err)
})
adapterPubClient.on('ready', () => {
logger.info('Redis adapter pub client ready')
})
adapterSubClient.on('ready', () => {
logger.info('Redis adapter sub client ready')
})
await Promise.all([adapterPubClient.connect(), adapterSubClient.connect()])
io.adapter(createAdapter(adapterPubClient, adapterSubClient))
logger.info('Socket.IO Redis adapter connected - cross-pod broadcasting enabled')
} else {
logger.warn('REDIS_URL not configured - running in single-pod mode')
}
logger.info('Socket.IO server configured with:', {
allowedOrigins: allowedOrigins.length,
transports: ['websocket', 'polling'],
pingTimeout: PING_TIMEOUT_MS,
pingInterval: PING_INTERVAL_MS,
maxHttpBufferSize: MAX_HTTP_BUFFER_SIZE,
cookieSecure: isProd,
corsCredentials: true,
redisAdapter: !!env.REDIS_URL,
})
return io
}
/**
* Clean up Redis adapter connections.
* Call this during graceful shutdown.
*/
export async function shutdownSocketIOAdapter(): Promise<void> {
const closePromises: Promise<void>[] = []
if (adapterPubClient) {
closePromises.push(
adapterPubClient.quit().then(() => {
logger.info('Redis adapter pub client closed')
adapterPubClient = null
})
)
}
if (adapterSubClient) {
closePromises.push(
adapterSubClient.quit().then(() => {
logger.info('Redis adapter sub client closed')
adapterSubClient = null
})
)
}
if (closePromises.length > 0) {
await Promise.all(closePromises)
logger.info('Socket.IO Redis adapter shutdown complete')
}
}