@@ -4,6 +4,7 @@ import { StorageAdapterBlock, StoreEventsLog } from "./common";
4
4
import { storeEventsAbi } from "@latticexyz/store" ;
5
5
import { logSort } from "@latticexyz/common" ;
6
6
import { SocketRpcClient , getWebSocketRpcClient } from "viem/utils" ;
7
+ import { debug } from "./debug" ;
7
8
8
9
type WatchLogsInput = {
9
10
url : string ;
@@ -16,70 +17,103 @@ type WatchLogsResult = {
16
17
} ;
17
18
18
19
export function watchLogs ( { url, address, fromBlock } : WatchLogsInput ) : WatchLogsResult {
19
- // Buffer the live logs received until the gap from `startBlock` to `currentBlock` is closed
20
- let caughtUp = false ;
21
- const logBuffer : StoreEventsLog [ ] = [ ] ;
22
-
23
20
const topics = [
24
21
storeEventsAbi . flatMap ( ( event ) => encodeEventTopics ( { abi : [ event ] , eventName : event . name } ) ) ,
25
22
] as LogTopic [ ] ; // https://github.com/wevm/viem/blob/63a5ac86eb9a2962f7323b4cc76ef54f9f5ef7ed/src/actions/public/getLogs.ts#L171
26
23
24
+ let resumeBlock = fromBlock ;
25
+ let keepAliveInterval : ReturnType < typeof setTimeout > | undefined = undefined ;
27
26
const logs$ = new Observable < StorageAdapterBlock > ( ( subscriber ) => {
28
27
let client : SocketRpcClient < WebSocket > ;
29
- getWebSocketRpcClient ( url , { keepAlive : true , reconnect : true } ) . then ( async ( _client ) => {
30
- client = _client ;
31
- client . socket . addEventListener ( "error" , ( error ) =>
32
- subscriber . error ( { code : - 32603 , message : "WebSocket error" , data : error } ) ,
33
- ) ;
34
-
35
- // Start watching pending logs
36
- const subscriptionId : Hex = (
37
- await client . requestAsync ( {
38
- body : {
39
- method : "wiresaw_watchLogs" ,
40
- params : [ { address, topics } ] ,
41
- } ,
42
- } )
43
- ) . result ;
44
-
45
- // Listen for wiresaw_watchLogs subscription
46
- // Need to use low level methods since viem's socekt client only handles `eth_subscription` messages.
47
- // (https://github.com/wevm/viem/blob/f81d497f2afc11b9b81a79057d1f797694b69793/src/utils/rpc/socket.ts#L178)
48
- client . socket . addEventListener ( "message" , ( message ) => {
49
- const response = JSON . parse ( message . data ) ;
50
- if ( "error" in response ) {
51
- // Return JSON-RPC errors to the subscriber
52
- subscriber . error ( response . error ) ;
53
- return ;
54
- }
55
28
56
- // Parse the logs from wiresaw_watchLogs
57
- if ( "params" in response && response . params . subscription === subscriptionId ) {
58
- const logs : RpcLog [ ] = response . params . result ;
59
- const formattedLogs = logs . map ( ( log ) => formatLog ( log ) ) ;
60
- const parsedLogs = parseEventLogs ( { abi : storeEventsAbi , logs : formattedLogs } ) ;
61
- if ( caughtUp ) {
62
- const blockNumber = parsedLogs [ 0 ] . blockNumber ;
63
- subscriber . next ( { blockNumber, logs : parsedLogs } ) ;
64
- } else {
65
- logBuffer . push ( ...parsedLogs ) ;
29
+ function setupClient ( ) : void {
30
+ // Buffer the live logs received until the gap from `startBlock` to `currentBlock` is closed
31
+ let caughtUp = false ;
32
+ const logBuffer : StoreEventsLog [ ] = [ ] ;
33
+
34
+ getWebSocketRpcClient ( url , {
35
+ keepAlive : false , // keepAlive is handled below
36
+ } ) . then ( async ( _client ) => {
37
+ client = _client ;
38
+ client . socket . addEventListener ( "error" , ( error ) =>
39
+ subscriber . error ( { code : - 32603 , message : "WebSocket error" , data : error } ) ,
40
+ ) ;
41
+
42
+ // Start watching pending logs
43
+ const subscriptionId : Hex = (
44
+ await client . requestAsync ( {
45
+ body : {
46
+ method : "wiresaw_watchLogs" ,
47
+ params : [ { address, topics } ] ,
48
+ } ,
49
+ } )
50
+ ) . result ;
51
+
52
+ // Listen for wiresaw_watchLogs subscription
53
+ // Need to use low level methods since viem's socekt client only handles `eth_subscription` messages.
54
+ // (https://github.com/wevm/viem/blob/f81d497f2afc11b9b81a79057d1f797694b69793/src/utils/rpc/socket.ts#L178)
55
+ client . socket . addEventListener ( "message" , ( message ) => {
56
+ const response = JSON . parse ( message . data ) ;
57
+ if ( "error" in response ) {
58
+ // Return JSON-RPC errors to the subscriber
59
+ subscriber . error ( response . error ) ;
60
+ return ;
61
+ }
62
+
63
+ // Parse the logs from wiresaw_watchLogs
64
+ if ( "params" in response && response . params . subscription === subscriptionId ) {
65
+ const logs : RpcLog [ ] = response . params . result ;
66
+ const formattedLogs = logs . map ( ( log ) => formatLog ( log ) ) ;
67
+ const parsedLogs = parseEventLogs ( { abi : storeEventsAbi , logs : formattedLogs } ) ;
68
+ if ( caughtUp ) {
69
+ const blockNumber = parsedLogs [ 0 ] . blockNumber ;
70
+ subscriber . next ( { blockNumber, logs : parsedLogs } ) ;
71
+ resumeBlock = blockNumber + 1n ;
72
+ } else {
73
+ logBuffer . push ( ...parsedLogs ) ;
74
+ }
66
75
}
76
+ } ) ;
77
+
78
+ // Catch up to the pending logs
79
+ try {
80
+ const initialLogs = await fetchInitialLogs ( { client, address, fromBlock : resumeBlock , topics } ) ;
81
+ const logs = [ ...initialLogs , ...logBuffer ] . sort ( logSort ) ;
82
+ const blockNumber = logs . at ( - 1 ) ?. blockNumber ?? resumeBlock ;
83
+ subscriber . next ( { blockNumber, logs : initialLogs } ) ;
84
+ resumeBlock = blockNumber + 1n ;
85
+ caughtUp = true ;
86
+ } catch ( e ) {
87
+ subscriber . error ( "Could not fetch initial wiresaw logs" ) ;
67
88
}
89
+
90
+ // Keep websocket alive and reconnect if it's not alive anymore
91
+ keepAliveInterval = setInterval ( async ( ) => {
92
+ try {
93
+ await Promise . race ( [
94
+ client . requestAsync ( { body : { method : "net_version" } } ) ,
95
+ new Promise < void > ( ( _ , reject ) => {
96
+ setTimeout ( reject , 2000 ) ;
97
+ } ) ,
98
+ ] ) ;
99
+ } catch {
100
+ debug ( "Detected unresponsive websocket, reconnecting..." ) ;
101
+ clearInterval ( keepAliveInterval ) ;
102
+ client . close ( ) ;
103
+ setupClient ( ) ;
104
+ }
105
+ } , 3000 ) ;
68
106
} ) ;
107
+ }
69
108
70
- // Catch up to the pending logs
71
- try {
72
- const initialLogs = await fetchInitialLogs ( { client, address, fromBlock, topics } ) ;
73
- const logs = [ ...initialLogs , ...logBuffer ] . sort ( logSort ) ;
74
- const blockNumber = logs . at ( - 1 ) ?. blockNumber ?? fromBlock ;
75
- subscriber . next ( { blockNumber, logs : initialLogs } ) ;
76
- caughtUp = true ;
77
- } catch ( e ) {
78
- subscriber . error ( "Could not fetch initial wiresaw logs" ) ;
79
- }
80
- } ) ;
109
+ setupClient ( ) ;
81
110
82
- return ( ) => client ?. close ( ) ;
111
+ return ( ) => {
112
+ client ?. close ( ) ;
113
+ if ( keepAliveInterval != null ) {
114
+ clearInterval ( keepAliveInterval ) ;
115
+ }
116
+ } ;
83
117
} ) ;
84
118
85
119
return { logs$ } ;
0 commit comments