@@ -6,34 +6,79 @@ import {
6
6
useGetEventStream ,
7
7
} from "@squonk/account-server-client/event-stream" ;
8
8
9
+ import dayjs from "dayjs" ;
10
+ import utc from "dayjs/plugin/utc" ;
9
11
import { useAtom } from "jotai" ;
10
12
import { useSnackbar } from "notistack" ;
11
13
12
14
import { useASAuthorizationStatus } from "../../hooks/useIsAuthorized" ;
13
- import { getMessageFromEvent , protoBlobToText } from "../../protobuf/protobuf" ;
14
- import { eventStreamEnabledAtom } from "../../state/eventStream" ;
15
+ import { getMessageFromEvent } from "../../protobuf/protobuf" ;
16
+ import {
17
+ eventStreamEnabledAtom ,
18
+ useEventStream ,
19
+ webSocketStatusAtom ,
20
+ } from "../../state/eventStream" ;
21
+ import { useUnreadEventCount } from "../../state/notifications" ;
15
22
import { EventMessage } from "../eventMessages/EventMessage" ;
16
23
import { useIsEventStreamInstalled } from "./useIsEventStreamInstalled" ;
17
24
25
+ dayjs . extend ( utc ) ;
26
+
27
+ /**
28
+ * Builds WebSocket URL
29
+ */
30
+ const buildWebSocketUrl = ( location : string ) : string => {
31
+ const url = new URL ( location ) ;
32
+ url . protocol = "wss:" ;
33
+
34
+ // Add ordinal parameter to get all historical messages
35
+ url . searchParams . set ( "stream_from_ordinal" , "1" ) ;
36
+
37
+ return url . toString ( ) ;
38
+ } ;
39
+
40
+ /**
41
+ * Manages WebSocket connection for event stream and displays toast notifications
42
+ */
18
43
export const EventStream = ( ) => {
19
44
const isEventStreamInstalled = useIsEventStreamInstalled ( ) ;
20
45
const [ location , setLocation ] = useState < string | null > ( null ) ;
21
46
const { enqueueSnackbar } = useSnackbar ( ) ;
47
+ const { incrementCount } = useUnreadEventCount ( ) ;
22
48
const asRole = useASAuthorizationStatus ( ) ;
49
+ const { addEvent, isEventNewerThanSession, initializeSession } = useEventStream ( ) ;
23
50
24
51
const { data, error : streamError } = useGetEventStream ( {
25
52
query : { select : ( data ) => data . location , enabled : ! ! asRole && isEventStreamInstalled } ,
26
53
} ) ;
54
+
27
55
const { mutate : createEventStream } = useCreateEventStream ( {
28
- mutation : {
29
- onSuccess : ( eventStreamResponse ) => {
30
- setLocation ( eventStreamResponse . location ) ;
31
- } ,
32
- } ,
56
+ mutation : { onSuccess : ( eventStreamResponse ) => setLocation ( eventStreamResponse . location ) } ,
33
57
} ) ;
58
+
34
59
const [ eventStreamEnabled ] = useAtom ( eventStreamEnabledAtom ) ;
60
+ const [ , setWebSocketStatus ] = useAtom ( webSocketStatusAtom ) ;
61
+
62
+ const handleWebSocketMessage = useCallback (
63
+ ( event : MessageEvent ) => {
64
+ const message = getMessageFromEvent ( JSON . parse ( event . data ) ) ;
65
+
66
+ if (
67
+ message &&
68
+ addEvent ( message ) && // Only show toast for events newer than session start
69
+ isEventNewerThanSession ( message )
70
+ ) {
71
+ enqueueSnackbar ( < EventMessage message = { message } /> , {
72
+ variant : "default" ,
73
+ anchorOrigin : { horizontal : "right" , vertical : "bottom" } ,
74
+ autoHideDuration : 10_000 ,
75
+ } ) ;
76
+ incrementCount ( ) ;
77
+ }
78
+ } ,
79
+ [ enqueueSnackbar , incrementCount , addEvent , isEventNewerThanSession ] ,
80
+ ) ;
35
81
36
- // Define callbacks *before* useWebSocket hook
37
82
const handleWebSocketOpen = useCallback ( ( ) => {
38
83
enqueueSnackbar ( "Connected to event stream" , {
39
84
variant : "success" ,
@@ -43,19 +88,14 @@ export const EventStream = () => {
43
88
44
89
const handleWebSocketClose = useCallback (
45
90
( event : CloseEvent ) => {
46
- console . log ( event ) ;
47
- if ( event . wasClean ) {
48
- enqueueSnackbar ( "Disconnected from event stream" , {
49
- variant : "info" ,
50
- anchorOrigin : { horizontal : "right" , vertical : "bottom" } ,
51
- } ) ;
52
- } else {
53
- console . warn ( "EventStream: WebSocket closed unexpectedly." ) ;
54
- enqueueSnackbar ( "Event stream disconnected unexpectedly. Attempting to reconnect..." , {
55
- variant : "warning" ,
56
- anchorOrigin : { horizontal : "right" , vertical : "bottom" } ,
57
- } ) ;
58
- }
91
+ const message = event . wasClean
92
+ ? "Disconnected from event stream"
93
+ : "Event stream disconnected unexpectedly. Attempting to reconnect..." ;
94
+
95
+ enqueueSnackbar ( message , {
96
+ variant : event . wasClean ? "info" : "warning" ,
97
+ anchorOrigin : { horizontal : "right" , vertical : "bottom" } ,
98
+ } ) ;
59
99
} ,
60
100
[ enqueueSnackbar ] ,
61
101
) ;
@@ -67,42 +107,10 @@ export const EventStream = () => {
67
107
} ) ;
68
108
} , [ enqueueSnackbar ] ) ;
69
109
70
- const handleWebSocketMessage = useCallback (
71
- ( event : MessageEvent ) => {
72
- if ( event . data instanceof Blob ) {
73
- protoBlobToText ( event . data )
74
- . then ( ( textData ) => {
75
- const message = getMessageFromEvent ( textData ) ;
76
- if ( message ) {
77
- enqueueSnackbar ( < EventMessage message = { message } /> , {
78
- variant : "default" ,
79
- anchorOrigin : { horizontal : "right" , vertical : "bottom" } ,
80
- autoHideDuration : 10_000 ,
81
- } ) ;
82
- } else {
83
- console . warn (
84
- "Received event data could not be parsed into a known message type:" ,
85
- textData ,
86
- ) ;
87
- }
88
- } )
89
- . catch ( ( error ) => {
90
- console . error ( "Error processing protobuf message:" , error ) ;
91
- enqueueSnackbar ( "Error processing incoming event" , {
92
- variant : "error" ,
93
- anchorOrigin : { horizontal : "right" , vertical : "bottom" } ,
94
- } ) ;
95
- } ) ;
96
- } else {
97
- console . warn ( "Received non-Blob WebSocket message:" , event . data ) ;
98
- }
99
- } ,
100
- [ enqueueSnackbar ] ,
101
- ) ;
102
-
103
- const wsUrl = eventStreamEnabled && asRole ? ( location ?. replace ( "ws" , "wss" ) ?? null ) : null ;
110
+ // Build WebSocket URL
111
+ const wsUrl = eventStreamEnabled && asRole && location ? buildWebSocketUrl ( location ) : null ;
104
112
105
- useWebSocket ( wsUrl , {
113
+ const { readyState } = useWebSocket ( wsUrl , {
106
114
onOpen : handleWebSocketOpen ,
107
115
onClose : handleWebSocketClose ,
108
116
onError : handleWebSocketError ,
@@ -113,7 +121,11 @@ export const EventStream = () => {
113
121
reconnectInterval : 3000 ,
114
122
} ) ;
115
123
116
- // Effects can now safely use the hook results or return early based on auth
124
+ // Expose connection status for status indicator
125
+ useEffect ( ( ) => {
126
+ setWebSocketStatus ( readyState ) ;
127
+ } , [ readyState , setWebSocketStatus ] ) ;
128
+
117
129
useEffect ( ( ) => {
118
130
if ( asRole && data ) {
119
131
setLocation ( data ) ;
@@ -126,5 +138,10 @@ export const EventStream = () => {
126
138
}
127
139
} , [ asRole , streamError , createEventStream ] ) ;
128
140
141
+ // Initialize session on client side only
142
+ useEffect ( ( ) => {
143
+ initializeSession ( ) ;
144
+ } , [ initializeSession ] ) ;
145
+
129
146
return null ;
130
147
} ;
0 commit comments