1+ export interface EventListenerOptions {
2+ eventContractId : string ;
3+ startCursor ?: string ;
4+ onEvent : ( event : ContractEvent ) => void ;
5+ onError ?: ( error : Error ) => void ;
6+ }
7+
8+ export interface ContractEvent {
9+ id : string ;
10+ contractId : string ;
11+ type : string ;
12+ data : Record < string , unknown > ;
13+ timestamp : number ;
14+ cursor : string ;
15+ }
16+
17+ export type ConnectionStatus = "connecting" | "connected" | "disconnecting" | "disconnected" | "error" ;
18+
19+ export class EventMonitor {
20+ private ws : WebSocket | null = null ;
21+ private status : ConnectionStatus = "disconnected" ;
22+ private reconnectAttempts = 0 ;
23+ private maxReconnectAttempts = 5 ;
24+ private reconnectDelay = 1000 ;
25+ private options : EventListenerOptions | null = null ;
26+
27+ async connect ( options : EventListenerOptions ) : Promise < void > {
28+ this . options = options ;
29+ this . status = "connecting" ;
30+
31+ const wsUrl = this . buildWsUrl ( options . eventContractId , options . startCursor ) ;
32+
33+ try {
34+ this . ws = new WebSocket ( wsUrl ) ;
35+
36+ this . ws . onopen = ( ) => {
37+ this . status = "connected" ;
38+ this . reconnectAttempts = 0 ;
39+ } ;
40+
41+ this . ws . onmessage = ( event ) => {
42+ try {
43+ const data = JSON . parse ( event . data ) ;
44+ const contractEvent : ContractEvent = {
45+ id : data . id ,
46+ contractId : data . contractId || options . eventContractId ,
47+ type : data . type || data . topic || "unknown" ,
48+ data : data . data || { } ,
49+ timestamp : data . timestamp || Date . now ( ) ,
50+ cursor : data . cursor || "" ,
51+ } ;
52+ options . onEvent ( contractEvent ) ;
53+ } catch ( e ) {
54+ console . error ( "Failed to parse WebSocket message:" , e ) ;
55+ }
56+ } ;
57+
58+ this . ws . onerror = ( ) => {
59+ this . status = "error" ;
60+ options . onError ?.( new Error ( "WebSocket connection error" ) ) ;
61+ } ;
62+
63+ this . ws . onclose = ( ) => {
64+ this . status = "disconnected" ;
65+ this . attemptReconnect ( ) ;
66+ } ;
67+ } catch ( e ) {
68+ this . status = "error" ;
69+ throw e ;
70+ }
71+ }
72+
73+ private buildWsUrl ( contractId : string , cursor ?: string ) : string {
74+ const horizonUrl = process . env . NEXT_PUBLIC_HORIZON_URL || "https://horizon-testnet.stellar.org" ;
75+ const protocol = horizonUrl . includes ( "testnet" ) ? "wss" : "wss" ;
76+ return `${ protocol } ${
77+ horizonUrl . replace ( "https://" , "" ) . replace ( "http://" , "" )
78+ } /events?contractId=${ contractId } ${ cursor ? `&cursor=${ cursor } ` : "" } `;
79+ }
80+
81+ private attemptReconnect ( ) : void {
82+ if ( this . options && this . reconnectAttempts < this . maxReconnectAttempts ) {
83+ this . reconnectAttempts ++ ;
84+ setTimeout ( ( ) => {
85+ this . options && this . connect ( this . options ) ;
86+ } , this . reconnectDelay * Math . pow ( 2 , this . reconnectAttempts - 1 ) ) ;
87+ }
88+ }
89+
90+ disconnect ( ) : void {
91+ if ( this . ws ) {
92+ this . status = "disconnecting" ;
93+ this . ws . close ( ) ;
94+ this . ws = null ;
95+ }
96+ this . status = "disconnected" ;
97+ }
98+
99+ getStatus ( ) : ConnectionStatus {
100+ return this . status ;
101+ }
102+ }
103+
104+ export function createEventMonitor ( ) : EventMonitor {
105+ return new EventMonitor ( ) ;
106+ }
0 commit comments