Pedro de Carvalho commited on
Commit
0c380b8
·
1 Parent(s): 7e21f09

Add socket service

Browse files
Files changed (1) hide show
  1. client/src/services/socket.ts +118 -0
client/src/services/socket.ts ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { WSMessage } from 'shared/types.ts'
2
+
3
+ let ws: WebSocket | null = null
4
+ let reconnectTimer: ReturnType<typeof setTimeout> | null = null
5
+ let intentionalClose = false
6
+ const pendingQueue: string[] = []
7
+
8
+ const WS_URL = import.meta.env.VITE_WS_URL || (
9
+ window.location.protocol === 'https:'
10
+ ? `wss://server.caro5.test/ws`
11
+ : `ws://localhost:8888`
12
+ )
13
+
14
+ type MessageHandler = (msg: WSMessage) => void
15
+ type ConnectionHandler = (status: 'connecting' | 'connected' | 'disconnected') => void
16
+ const handlers = new Set<MessageHandler>()
17
+ const reconnectHandlers = new Set<() => void>()
18
+ const connectionHandlers = new Set<ConnectionHandler>()
19
+ let connectionStatus: 'connecting' | 'connected' | 'disconnected' = 'disconnected'
20
+
21
+ function ensureConnected(): void {
22
+ if (!intentionalClose) {
23
+ connect()
24
+ }
25
+ }
26
+
27
+ function flushQueue(): void {
28
+ while (pendingQueue.length && ws?.readyState === WebSocket.OPEN) {
29
+ ws.send(pendingQueue.shift()!)
30
+ }
31
+ }
32
+
33
+ function notifyConnection(): void {
34
+ connectionHandlers.forEach((fn) => fn(connectionStatus))
35
+ }
36
+
37
+ function connect(): void {
38
+ if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return
39
+
40
+ intentionalClose = false
41
+ connectionStatus = 'connecting'
42
+ notifyConnection()
43
+ ws = new WebSocket(WS_URL)
44
+
45
+ ws.onopen = () => {
46
+ console.log('[socket] connected')
47
+ connectionStatus = 'connected'
48
+ notifyConnection()
49
+ flushQueue()
50
+ reconnectHandlers.forEach((fn) => fn())
51
+ }
52
+
53
+ ws.onmessage = (event: MessageEvent) => {
54
+ try {
55
+ const msg = JSON.parse(event.data) as WSMessage
56
+ handlers.forEach((fn) => fn(msg))
57
+ } catch {
58
+ console.warn('[socket] failed to parse message')
59
+ }
60
+ }
61
+
62
+ ws.onclose = () => {
63
+ connectionStatus = 'disconnected'
64
+ notifyConnection()
65
+ ws = null
66
+ if (!intentionalClose) {
67
+ reconnectTimer = setTimeout(connect, 2000)
68
+ }
69
+ }
70
+
71
+ ws.onerror = () => {
72
+ ws?.close()
73
+ }
74
+ }
75
+
76
+ export function disconnect(): void {
77
+ intentionalClose = true
78
+ if (reconnectTimer) {
79
+ clearTimeout(reconnectTimer)
80
+ reconnectTimer = null
81
+ }
82
+ handlers.clear()
83
+ pendingQueue.length = 0
84
+ ws?.close()
85
+ ws = null
86
+ }
87
+
88
+ export function sendMessage(type: string, payload: unknown = {}): void {
89
+ const data = JSON.stringify({ type, payload })
90
+ if (ws?.readyState === WebSocket.OPEN) {
91
+ ws.send(data)
92
+ } else {
93
+ ensureConnected()
94
+ pendingQueue.push(data)
95
+ }
96
+ }
97
+
98
+ export function onMessage(fn: MessageHandler): () => void {
99
+ handlers.add(fn)
100
+ ensureConnected()
101
+ return () => handlers.delete(fn)
102
+ }
103
+
104
+ export function onReconnect(fn: () => void): () => void {
105
+ reconnectHandlers.add(fn)
106
+ ensureConnected()
107
+ return () => reconnectHandlers.delete(fn)
108
+ }
109
+
110
+ export function onConnectionChange(fn: ConnectionHandler): () => void {
111
+ connectionHandlers.add(fn)
112
+ ensureConnected()
113
+ return () => connectionHandlers.delete(fn)
114
+ }
115
+
116
+ export function getConnectionStatus(): 'connecting' | 'connected' | 'disconnected' {
117
+ return connectionStatus
118
+ }