Skip to content

Commit 660fba5

Browse files
committed
merge: alvs/ws-keepalive
1 parent fad4cda commit 660fba5

File tree

1 file changed

+86
-52
lines changed

1 file changed

+86
-52
lines changed

packages/store-sync/src/wiresaw.ts

Lines changed: 86 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { StorageAdapterBlock, StoreEventsLog } from "./common";
44
import { storeEventsAbi } from "@latticexyz/store";
55
import { logSort } from "@latticexyz/common";
66
import { SocketRpcClient, getWebSocketRpcClient } from "viem/utils";
7+
import { debug } from "./debug";
78

89
type WatchLogsInput = {
910
url: string;
@@ -16,70 +17,103 @@ type WatchLogsResult = {
1617
};
1718

1819
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-
2320
const topics = [
2421
storeEventsAbi.flatMap((event) => encodeEventTopics({ abi: [event], eventName: event.name })),
2522
] as LogTopic[]; // https://github.com/wevm/viem/blob/63a5ac86eb9a2962f7323b4cc76ef54f9f5ef7ed/src/actions/public/getLogs.ts#L171
2623

24+
let resumeBlock = fromBlock;
25+
let keepAliveInterval: ReturnType<typeof setTimeout> | undefined = undefined;
2726
const logs$ = new Observable<StorageAdapterBlock>((subscriber) => {
2827
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-
}
5528

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+
}
6675
}
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");
6788
}
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);
68106
});
107+
}
69108

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();
81110

82-
return () => client?.close();
111+
return () => {
112+
client?.close();
113+
if (keepAliveInterval != null) {
114+
clearInterval(keepAliveInterval);
115+
}
116+
};
83117
});
84118

85119
return { logs$ };

0 commit comments

Comments
 (0)