From 805391509a0e9bc4f24db317fb120462aafec2b6 Mon Sep 17 00:00:00 2001 From: alvarius Date: Wed, 16 Jul 2025 13:01:40 +0200 Subject: [PATCH 1/9] wip: add reproduction logs --- environments/vite/src/main.ts | 24 +++++++++--------------- src/utils/rpc/socket.ts | 35 ++++++++++++++++++++++++++++------- 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/environments/vite/src/main.ts b/environments/vite/src/main.ts index 2000313fe5..d546ca96ae 100644 --- a/environments/vite/src/main.ts +++ b/environments/vite/src/main.ts @@ -1,19 +1,13 @@ -import { http, createPublicClient, webSocket } from 'viem' -import { mainnet } from 'viem/chains' - -const client = createPublicClient({ - chain: mainnet, - transport: http(), -}) +import { createPublicClient, webSocket } from 'viem' +import { redstone } from 'viem/chains' const webSocketClient = createPublicClient({ - chain: mainnet, - transport: webSocket( - 'wss://eth-mainnet.g.alchemy.com/v2/WV-bLot1hKjjCfpPq603Ro-jViFzwYX8', - ), + chain: redstone, + transport: webSocket(), }) -await client.getBlockNumber() -await webSocketClient.getBlockNumber() - -document.getElementById('app')!.innerText = 'success' +setInterval(async () => { + const blockNumber = await webSocketClient.getBlockNumber() + console.log(blockNumber) + document.getElementById('app')!.innerText = `Block number: ${blockNumber}` +}, 1000) \ No newline at end of file diff --git a/src/utils/rpc/socket.ts b/src/utils/rpc/socket.ts index 6cf5eb46a9..f3497074c9 100644 --- a/src/utils/rpc/socket.ts +++ b/src/utils/rpc/socket.ts @@ -113,9 +113,13 @@ export async function getSocketRpcClient( let socketClient = socketClientCache.get(id) // If the socket already exists, return it. - if (socketClient) return socketClient as {} as SocketRpcClient - + if (socketClient) { + console.log("returning existing socket client", { id }); + return socketClient as {} as SocketRpcClient + } let reconnectCount = 0 + let setupCount = 0 + let reconnectScheduled = false; const { schedule } = createBatchScheduler< undefined, [SocketRpcClient] @@ -134,8 +138,12 @@ export async function getSocketRpcClient( // Set up socket implementation. async function setup() { + reconnectScheduled = false; + const setupId = setupCount++; + console.log("new setup", { setupId, id }); const result = await getSocket({ onClose() { + console.log("socket closed", { setupId, id }); // Notify all requests and subscriptions of the closure error. for (const request of requests.values()) request.onError?.(new SocketClosedError({ url })) @@ -143,11 +151,15 @@ export async function getSocketRpcClient( subscription.onError?.(new SocketClosedError({ url })) // Attempt to reconnect. - if (reconnect && reconnectCount < attempts) + if (reconnect && reconnectCount < attempts && !reconnectScheduled) { + reconnectScheduled = true; + reconnectCount++ + console.log("starting timeout after close", { reconnectCount, setupId, id }); setTimeout(async () => { - reconnectCount++ + console.log("calling setup after close", { reconnectCount, setupId, id }); await setup().catch(console.error) }, delay) + } // Otherwise, clear all requests and subscriptions. else { requests.clear() @@ -163,14 +175,21 @@ export async function getSocketRpcClient( subscription.onError?.(error) // Make sure socket is definitely closed. - socketClient?.close() + console.log("got error, closing socket", { setupId, id }); + // This is clearing the cache, which means `getSocketRpcClient` will call a fresh `setup` again. + // This is also triggering the `onClose` callback, which will trigger a new `setup` call as well. + socketClient?.close() // Attempt to reconnect. - if (reconnect && reconnectCount < attempts) + if (reconnect && reconnectCount < attempts && !reconnectScheduled) { + reconnectScheduled = true; + reconnectCount++ + console.log("starting timeout after error", { reconnectCount, setupId, id }); setTimeout(async () => { - reconnectCount++ + console.log("calling setup after error", { reconnectCount, setupId, id }); await setup().catch(console.error) }, delay) + } // Otherwise, clear all requests and subscriptions. else { requests.clear() @@ -213,12 +232,14 @@ export async function getSocketRpcClient( return result } + console.log("calling fresh setup", { id }); await setup() error = undefined // Create a new socket instance. socketClient = { close() { + console.log("closing socket and clearing cache", { id }); keepAliveTimer && clearInterval(keepAliveTimer) socket.close() socketClientCache.delete(id) From 7d7d0771b2480e7ab067873d5f7be073b820db55 Mon Sep 17 00:00:00 2001 From: alvarius Date: Wed, 16 Jul 2025 13:51:31 +0200 Subject: [PATCH 2/9] skip reconnection if reconnection in progress --- src/utils/rpc/socket.ts | 112 ++++++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/src/utils/rpc/socket.ts b/src/utils/rpc/socket.ts index f3497074c9..5f55cdaaa1 100644 --- a/src/utils/rpc/socket.ts +++ b/src/utils/rpc/socket.ts @@ -114,12 +114,12 @@ export async function getSocketRpcClient( // If the socket already exists, return it. if (socketClient) { - console.log("returning existing socket client", { id }); + // biome-ignore lint/suspicious/noConsoleLog: + console.log('returning existing socket client', { id }) return socketClient as {} as SocketRpcClient } let reconnectCount = 0 let setupCount = 0 - let reconnectScheduled = false; const { schedule } = createBatchScheduler< undefined, [SocketRpcClient] @@ -135,68 +135,66 @@ export async function getSocketRpcClient( let error: Error | Event | undefined let socket: Socket<{}> let keepAliveTimer: ReturnType | undefined + let reconnectScheduled = false + + async function handleError(error: Error | Event | undefined) { + // Notify all requests and subscriptions of the error. + for (const request of requests.values()) request.onError?.(error) + for (const subscription of subscriptions.values()) + subscription.onError?.(error) + + // Attempt to reconnect. + if (reconnect && reconnectCount < attempts) { + if (!reconnectScheduled) { + // biome-ignore lint/suspicious/noConsoleLog: + console.log('scheduling reconnect', { reconnectCount, id }) + reconnectScheduled = true + reconnectCount++ + setTimeout(async () => { + // biome-ignore lint/suspicious/noConsoleLog: + console.log('reconnecting', { reconnectCount, id }) + await setup().catch(console.error) + reconnectScheduled = false + }, delay) + } else { + // biome-ignore lint/suspicious/noConsoleLog: + console.log('reconnect already scheduled', { reconnectCount, id }) + } + } + // Otherwise, clear all requests and subscriptions. + else { + // biome-ignore lint/suspicious/noConsoleLog: + console.log('giving up on reconnect', { reconnectCount, id }) + requests.clear() + subscriptions.clear() + } + } // Set up socket implementation. async function setup() { - reconnectScheduled = false; - const setupId = setupCount++; - console.log("new setup", { setupId, id }); + const setupId = setupCount++ + // biome-ignore lint/suspicious/noConsoleLog: + console.log('new setup', { setupId, id }) const result = await getSocket({ onClose() { - console.log("socket closed", { setupId, id }); - // Notify all requests and subscriptions of the closure error. - for (const request of requests.values()) - request.onError?.(new SocketClosedError({ url })) - for (const subscription of subscriptions.values()) - subscription.onError?.(new SocketClosedError({ url })) - - // Attempt to reconnect. - if (reconnect && reconnectCount < attempts && !reconnectScheduled) { - reconnectScheduled = true; - reconnectCount++ - console.log("starting timeout after close", { reconnectCount, setupId, id }); - setTimeout(async () => { - console.log("calling setup after close", { reconnectCount, setupId, id }); - await setup().catch(console.error) - }, delay) - } - // Otherwise, clear all requests and subscriptions. - else { - requests.clear() - subscriptions.clear() - } + // biome-ignore lint/suspicious/noConsoleLog: + console.log('onClose', { setupId, id }) + handleError(new SocketClosedError({ url })) }, onError(error_) { error = error_ - - // Notify all requests and subscriptions of the error. - for (const request of requests.values()) request.onError?.(error) - for (const subscription of subscriptions.values()) - subscription.onError?.(error) - - // Make sure socket is definitely closed. - console.log("got error, closing socket", { setupId, id }); - // This is clearing the cache, which means `getSocketRpcClient` will call a fresh `setup` again. - // This is also triggering the `onClose` callback, which will trigger a new `setup` call as well. - socketClient?.close() - - // Attempt to reconnect. - if (reconnect && reconnectCount < attempts && !reconnectScheduled) { - reconnectScheduled = true; - reconnectCount++ - console.log("starting timeout after error", { reconnectCount, setupId, id }); - setTimeout(async () => { - console.log("calling setup after error", { reconnectCount, setupId, id }); - await setup().catch(console.error) - }, delay) - } - // Otherwise, clear all requests and subscriptions. - else { - requests.clear() - subscriptions.clear() - } + // biome-ignore lint/suspicious/noConsoleLog: + console.log('onError, closing socket', { + setupId, + id, + error: error_, + }) + socket?.close() + handleError(error_) }, onOpen() { + // biome-ignore lint/suspicious/noConsoleLog: + console.log('socket opened', { setupId, id }) error = undefined reconnectCount = 0 }, @@ -232,14 +230,16 @@ export async function getSocketRpcClient( return result } - console.log("calling fresh setup", { id }); + // biome-ignore lint/suspicious/noConsoleLog: + console.log('calling fresh setup', { id }) await setup() error = undefined // Create a new socket instance. socketClient = { close() { - console.log("closing socket and clearing cache", { id }); + // biome-ignore lint/suspicious/noConsoleLog: + console.log('closing socket and clearing cache', { id }) keepAliveTimer && clearInterval(keepAliveTimer) socket.close() socketClientCache.delete(id) From 66257ebfd17dbaafa419087555e59c7eea1bb405 Mon Sep 17 00:00:00 2001 From: alvarius Date: Wed, 16 Jul 2025 15:46:13 +0200 Subject: [PATCH 3/9] wip --- environments/vite/src/main.ts | 3 +- src/utils/rpc/socket.ts | 122 +++++++++++++++++++--------------- 2 files changed, 69 insertions(+), 56 deletions(-) diff --git a/environments/vite/src/main.ts b/environments/vite/src/main.ts index d546ca96ae..e71cdd65df 100644 --- a/environments/vite/src/main.ts +++ b/environments/vite/src/main.ts @@ -8,6 +8,5 @@ const webSocketClient = createPublicClient({ setInterval(async () => { const blockNumber = await webSocketClient.getBlockNumber() - console.log(blockNumber) document.getElementById('app')!.innerText = `Block number: ${blockNumber}` -}, 1000) \ No newline at end of file +}, 1000) diff --git a/src/utils/rpc/socket.ts b/src/utils/rpc/socket.ts index 5f55cdaaa1..c550d70f4e 100644 --- a/src/utils/rpc/socket.ts +++ b/src/utils/rpc/socket.ts @@ -94,6 +94,8 @@ export const socketClientCache = /*#__PURE__*/ new Map< SocketRpcClient> >() +let setupCount = 0 + export async function getSocketRpcClient( parameters: GetSocketRpcClientParameters, ): Promise> { @@ -119,7 +121,6 @@ export async function getSocketRpcClient( return socketClient as {} as SocketRpcClient } let reconnectCount = 0 - let setupCount = 0 const { schedule } = createBatchScheduler< undefined, [SocketRpcClient] @@ -136,6 +137,7 @@ export async function getSocketRpcClient( let socket: Socket<{}> let keepAliveTimer: ReturnType | undefined let reconnectScheduled = false + let setupLock = false async function handleError(error: Error | Event | undefined) { // Notify all requests and subscriptions of the error. @@ -167,69 +169,81 @@ export async function getSocketRpcClient( console.log('giving up on reconnect', { reconnectCount, id }) requests.clear() subscriptions.clear() + setTimeout(() => socketClient?.close(), delay) } } // Set up socket implementation. async function setup() { - const setupId = setupCount++ - // biome-ignore lint/suspicious/noConsoleLog: - console.log('new setup', { setupId, id }) - const result = await getSocket({ - onClose() { - // biome-ignore lint/suspicious/noConsoleLog: - console.log('onClose', { setupId, id }) - handleError(new SocketClosedError({ url })) - }, - onError(error_) { - error = error_ - // biome-ignore lint/suspicious/noConsoleLog: - console.log('onError, closing socket', { - setupId, - id, - error: error_, - }) - socket?.close() - handleError(error_) - }, - onOpen() { - // biome-ignore lint/suspicious/noConsoleLog: - console.log('socket opened', { setupId, id }) - error = undefined - reconnectCount = 0 - }, - onResponse(data) { - const isSubscription = data.method === 'eth_subscription' - const id = isSubscription ? data.params.subscription : data.id - const cache = isSubscription ? subscriptions : requests - const callback = cache.get(id) - if (callback) callback.onResponse(data) - if (!isSubscription) cache.delete(id) - }, - }) - - socket = result - - if (keepAlive) { - if (keepAliveTimer) clearInterval(keepAliveTimer) - keepAliveTimer = setInterval(() => socket.ping?.(), keepAliveInterval) - } + if (setupLock) return + try { + setupLock = true + + const setupId = setupCount++ + // biome-ignore lint/suspicious/noConsoleLog: + console.log('new setup', { setupId, reconnectCount }) + const result = await getSocket({ + onClose() { + // biome-ignore lint/suspicious/noConsoleLog: + console.log('onClose', { setupId, id }) + handleError(new SocketClosedError({ url })) + }, + onError(error_) { + error = error_ + // biome-ignore lint/suspicious/noConsoleLog: + console.log('onError, closing socket', { + setupId, + id, + error: error_, + }) + socket?.close() + handleError(error_) + }, + onOpen() { + // biome-ignore lint/suspicious/noConsoleLog: + console.log('socket opened', { setupId, id }) + error = undefined + reconnectCount = 0 + }, + onResponse(data) { + const isSubscription = data.method === 'eth_subscription' + const id = isSubscription ? data.params.subscription : data.id + const cache = isSubscription ? subscriptions : requests + const callback = cache.get(id) + if (callback) callback.onResponse(data) + if (!isSubscription) cache.delete(id) + }, + }) + + socket = result - if (reconnect && subscriptions.size > 0) { - const subscriptionEntries = subscriptions.entries() - for (const [ - key, - { onResponse, body, onError }, - ] of subscriptionEntries) { - if (!body) continue + if (keepAlive) { + if (keepAliveTimer) clearInterval(keepAliveTimer) + keepAliveTimer = setInterval( + () => socket.ping?.(), + keepAliveInterval, + ) + } + + if (reconnect && subscriptions.size > 0) { + const subscriptionEntries = subscriptions.entries() + for (const [ + key, + { onResponse, body, onError }, + ] of subscriptionEntries) { + if (!body) continue - subscriptions.delete(key) - socketClient?.request({ body, onResponse, onError }) + subscriptions.delete(key) + socketClient?.request({ body, onResponse, onError }) + } } - } - return result + return result + } finally { + setupLock = false + } } + // biome-ignore lint/suspicious/noConsoleLog: console.log('calling fresh setup', { id }) await setup() From 74fd7fb44dbe915953ff7744ccc5db001a4ceaf5 Mon Sep 17 00:00:00 2001 From: alvarius Date: Wed, 16 Jul 2025 16:27:06 +0200 Subject: [PATCH 4/9] reset to baseline --- src/utils/rpc/socket.ts | 187 ++++++++++++++++------------------------ 1 file changed, 76 insertions(+), 111 deletions(-) diff --git a/src/utils/rpc/socket.ts b/src/utils/rpc/socket.ts index c550d70f4e..6cf5eb46a9 100644 --- a/src/utils/rpc/socket.ts +++ b/src/utils/rpc/socket.ts @@ -94,8 +94,6 @@ export const socketClientCache = /*#__PURE__*/ new Map< SocketRpcClient> >() -let setupCount = 0 - export async function getSocketRpcClient( parameters: GetSocketRpcClientParameters, ): Promise> { @@ -115,11 +113,8 @@ export async function getSocketRpcClient( let socketClient = socketClientCache.get(id) // If the socket already exists, return it. - if (socketClient) { - // biome-ignore lint/suspicious/noConsoleLog: - console.log('returning existing socket client', { id }) - return socketClient as {} as SocketRpcClient - } + if (socketClient) return socketClient as {} as SocketRpcClient + let reconnectCount = 0 const { schedule } = createBatchScheduler< undefined, @@ -136,124 +131,94 @@ export async function getSocketRpcClient( let error: Error | Event | undefined let socket: Socket<{}> let keepAliveTimer: ReturnType | undefined - let reconnectScheduled = false - let setupLock = false - - async function handleError(error: Error | Event | undefined) { - // Notify all requests and subscriptions of the error. - for (const request of requests.values()) request.onError?.(error) - for (const subscription of subscriptions.values()) - subscription.onError?.(error) - - // Attempt to reconnect. - if (reconnect && reconnectCount < attempts) { - if (!reconnectScheduled) { - // biome-ignore lint/suspicious/noConsoleLog: - console.log('scheduling reconnect', { reconnectCount, id }) - reconnectScheduled = true - reconnectCount++ - setTimeout(async () => { - // biome-ignore lint/suspicious/noConsoleLog: - console.log('reconnecting', { reconnectCount, id }) - await setup().catch(console.error) - reconnectScheduled = false - }, delay) - } else { - // biome-ignore lint/suspicious/noConsoleLog: - console.log('reconnect already scheduled', { reconnectCount, id }) - } - } - // Otherwise, clear all requests and subscriptions. - else { - // biome-ignore lint/suspicious/noConsoleLog: - console.log('giving up on reconnect', { reconnectCount, id }) - requests.clear() - subscriptions.clear() - setTimeout(() => socketClient?.close(), delay) - } - } // Set up socket implementation. async function setup() { - if (setupLock) return - try { - setupLock = true - - const setupId = setupCount++ - // biome-ignore lint/suspicious/noConsoleLog: - console.log('new setup', { setupId, reconnectCount }) - const result = await getSocket({ - onClose() { - // biome-ignore lint/suspicious/noConsoleLog: - console.log('onClose', { setupId, id }) - handleError(new SocketClosedError({ url })) - }, - onError(error_) { - error = error_ - // biome-ignore lint/suspicious/noConsoleLog: - console.log('onError, closing socket', { - setupId, - id, - error: error_, - }) - socket?.close() - handleError(error_) - }, - onOpen() { - // biome-ignore lint/suspicious/noConsoleLog: - console.log('socket opened', { setupId, id }) - error = undefined - reconnectCount = 0 - }, - onResponse(data) { - const isSubscription = data.method === 'eth_subscription' - const id = isSubscription ? data.params.subscription : data.id - const cache = isSubscription ? subscriptions : requests - const callback = cache.get(id) - if (callback) callback.onResponse(data) - if (!isSubscription) cache.delete(id) - }, - }) - - socket = result - - if (keepAlive) { - if (keepAliveTimer) clearInterval(keepAliveTimer) - keepAliveTimer = setInterval( - () => socket.ping?.(), - keepAliveInterval, - ) - } + const result = await getSocket({ + onClose() { + // Notify all requests and subscriptions of the closure error. + for (const request of requests.values()) + request.onError?.(new SocketClosedError({ url })) + for (const subscription of subscriptions.values()) + subscription.onError?.(new SocketClosedError({ url })) + + // Attempt to reconnect. + if (reconnect && reconnectCount < attempts) + setTimeout(async () => { + reconnectCount++ + await setup().catch(console.error) + }, delay) + // Otherwise, clear all requests and subscriptions. + else { + requests.clear() + subscriptions.clear() + } + }, + onError(error_) { + error = error_ + + // Notify all requests and subscriptions of the error. + for (const request of requests.values()) request.onError?.(error) + for (const subscription of subscriptions.values()) + subscription.onError?.(error) + + // Make sure socket is definitely closed. + socketClient?.close() + + // Attempt to reconnect. + if (reconnect && reconnectCount < attempts) + setTimeout(async () => { + reconnectCount++ + await setup().catch(console.error) + }, delay) + // Otherwise, clear all requests and subscriptions. + else { + requests.clear() + subscriptions.clear() + } + }, + onOpen() { + error = undefined + reconnectCount = 0 + }, + onResponse(data) { + const isSubscription = data.method === 'eth_subscription' + const id = isSubscription ? data.params.subscription : data.id + const cache = isSubscription ? subscriptions : requests + const callback = cache.get(id) + if (callback) callback.onResponse(data) + if (!isSubscription) cache.delete(id) + }, + }) + + socket = result + + if (keepAlive) { + if (keepAliveTimer) clearInterval(keepAliveTimer) + keepAliveTimer = setInterval(() => socket.ping?.(), keepAliveInterval) + } - if (reconnect && subscriptions.size > 0) { - const subscriptionEntries = subscriptions.entries() - for (const [ - key, - { onResponse, body, onError }, - ] of subscriptionEntries) { - if (!body) continue + if (reconnect && subscriptions.size > 0) { + const subscriptionEntries = subscriptions.entries() + for (const [ + key, + { onResponse, body, onError }, + ] of subscriptionEntries) { + if (!body) continue - subscriptions.delete(key) - socketClient?.request({ body, onResponse, onError }) - } + subscriptions.delete(key) + socketClient?.request({ body, onResponse, onError }) } - - return result - } finally { - setupLock = false } - } - // biome-ignore lint/suspicious/noConsoleLog: - console.log('calling fresh setup', { id }) + return result + } await setup() error = undefined // Create a new socket instance. socketClient = { close() { - // biome-ignore lint/suspicious/noConsoleLog: - console.log('closing socket and clearing cache', { id }) keepAliveTimer && clearInterval(keepAliveTimer) socket.close() socketClientCache.delete(id) From 566f6379a14277094b6ea79c387fecd3c61d1cf2 Mon Sep 17 00:00:00 2001 From: alvarius Date: Wed, 16 Jul 2025 16:50:19 +0200 Subject: [PATCH 5/9] add reconnect lock --- environments/vite/src/main.ts | 8 ++++-- src/utils/rpc/socket.ts | 54 +++++++++++++++++++---------------- 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/environments/vite/src/main.ts b/environments/vite/src/main.ts index e71cdd65df..938337c2e2 100644 --- a/environments/vite/src/main.ts +++ b/environments/vite/src/main.ts @@ -7,6 +7,10 @@ const webSocketClient = createPublicClient({ }) setInterval(async () => { - const blockNumber = await webSocketClient.getBlockNumber() - document.getElementById('app')!.innerText = `Block number: ${blockNumber}` + try { + const blockNumber = await webSocketClient.getBlockNumber() + document.getElementById('app')!.innerText = `Block number: ${blockNumber}` + } catch (e) { + console.error(e) + } }, 1000) diff --git a/src/utils/rpc/socket.ts b/src/utils/rpc/socket.ts index 6cf5eb46a9..1c2503c11a 100644 --- a/src/utils/rpc/socket.ts +++ b/src/utils/rpc/socket.ts @@ -94,6 +94,9 @@ export const socketClientCache = /*#__PURE__*/ new Map< SocketRpcClient> >() +// TODO: remove, just for debugging +let setupCount = 0 + export async function getSocketRpcClient( parameters: GetSocketRpcClientParameters, ): Promise> { @@ -132,8 +135,31 @@ export async function getSocketRpcClient( let socket: Socket<{}> let keepAliveTimer: ReturnType | undefined + let reconnectLock = false + function attemptReconnect() { + if (reconnect && reconnectCount < attempts) { + if (reconnectLock) return + reconnectLock = true + reconnectCount++ + setTimeout(async () => { + await setup().catch(console.error) + reconnectLock = false + }, delay) + } else { + // TODO: remove, just for debugging + // biome-ignore lint/suspicious/noConsoleLog: + console.log('give up reconnect') + requests.clear() + subscriptions.clear() + } + } + // Set up socket implementation. async function setup() { + // TODO: remove, just for debugging + // biome-ignore lint/suspicious/noConsoleLog: + console.log('setup', { setupCount, reconnectCount }) + const result = await getSocket({ onClose() { // Notify all requests and subscriptions of the closure error. @@ -142,17 +168,7 @@ export async function getSocketRpcClient( for (const subscription of subscriptions.values()) subscription.onError?.(new SocketClosedError({ url })) - // Attempt to reconnect. - if (reconnect && reconnectCount < attempts) - setTimeout(async () => { - reconnectCount++ - await setup().catch(console.error) - }, delay) - // Otherwise, clear all requests and subscriptions. - else { - requests.clear() - subscriptions.clear() - } + attemptReconnect() }, onError(error_) { error = error_ @@ -163,19 +179,9 @@ export async function getSocketRpcClient( subscription.onError?.(error) // Make sure socket is definitely closed. - socketClient?.close() - - // Attempt to reconnect. - if (reconnect && reconnectCount < attempts) - setTimeout(async () => { - reconnectCount++ - await setup().catch(console.error) - }, delay) - // Otherwise, clear all requests and subscriptions. - else { - requests.clear() - subscriptions.clear() - } + socket?.close() + + attemptReconnect() }, onOpen() { error = undefined From 7ec49e34445c9349f38290b0516840c0746d6e9a Mon Sep 17 00:00:00 2001 From: alvarius Date: Wed, 16 Jul 2025 17:31:23 +0200 Subject: [PATCH 6/9] remove logs --- environments/vite/src/main.ts | 7 +++++-- src/utils/rpc/socket.ts | 15 ++------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/environments/vite/src/main.ts b/environments/vite/src/main.ts index 938337c2e2..b71dd32e35 100644 --- a/environments/vite/src/main.ts +++ b/environments/vite/src/main.ts @@ -1,9 +1,12 @@ -import { createPublicClient, webSocket } from 'viem' +import { createPublicClient, webSocket, fallback, http } from 'viem' import { redstone } from 'viem/chains' const webSocketClient = createPublicClient({ chain: redstone, - transport: webSocket(), + transport: fallback([ + webSocket(undefined, { reconnect: { attempts: 100, delay: 1000 } }), + http(), + ]), }) setInterval(async () => { diff --git a/src/utils/rpc/socket.ts b/src/utils/rpc/socket.ts index 1c2503c11a..8057968c9a 100644 --- a/src/utils/rpc/socket.ts +++ b/src/utils/rpc/socket.ts @@ -94,9 +94,6 @@ export const socketClientCache = /*#__PURE__*/ new Map< SocketRpcClient> >() -// TODO: remove, just for debugging -let setupCount = 0 - export async function getSocketRpcClient( parameters: GetSocketRpcClientParameters, ): Promise> { @@ -141,14 +138,13 @@ export async function getSocketRpcClient( if (reconnectLock) return reconnectLock = true reconnectCount++ + // Make sure the previous socket is definitely closed. + socket?.close() setTimeout(async () => { await setup().catch(console.error) reconnectLock = false }, delay) } else { - // TODO: remove, just for debugging - // biome-ignore lint/suspicious/noConsoleLog: - console.log('give up reconnect') requests.clear() subscriptions.clear() } @@ -156,10 +152,6 @@ export async function getSocketRpcClient( // Set up socket implementation. async function setup() { - // TODO: remove, just for debugging - // biome-ignore lint/suspicious/noConsoleLog: - console.log('setup', { setupCount, reconnectCount }) - const result = await getSocket({ onClose() { // Notify all requests and subscriptions of the closure error. @@ -178,9 +170,6 @@ export async function getSocketRpcClient( for (const subscription of subscriptions.values()) subscription.onError?.(error) - // Make sure socket is definitely closed. - socket?.close() - attemptReconnect() }, onOpen() { From 89ea6af7d9c2934fc853e4aee7be400de65bffc6 Mon Sep 17 00:00:00 2001 From: alvarius Date: Wed, 16 Jul 2025 17:36:02 +0200 Subject: [PATCH 7/9] remove reproduction environment --- environments/vite/src/main.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/environments/vite/src/main.ts b/environments/vite/src/main.ts index b71dd32e35..2000313fe5 100644 --- a/environments/vite/src/main.ts +++ b/environments/vite/src/main.ts @@ -1,19 +1,19 @@ -import { createPublicClient, webSocket, fallback, http } from 'viem' -import { redstone } from 'viem/chains' +import { http, createPublicClient, webSocket } from 'viem' +import { mainnet } from 'viem/chains' + +const client = createPublicClient({ + chain: mainnet, + transport: http(), +}) const webSocketClient = createPublicClient({ - chain: redstone, - transport: fallback([ - webSocket(undefined, { reconnect: { attempts: 100, delay: 1000 } }), - http(), - ]), + chain: mainnet, + transport: webSocket( + 'wss://eth-mainnet.g.alchemy.com/v2/WV-bLot1hKjjCfpPq603Ro-jViFzwYX8', + ), }) -setInterval(async () => { - try { - const blockNumber = await webSocketClient.getBlockNumber() - document.getElementById('app')!.innerText = `Block number: ${blockNumber}` - } catch (e) { - console.error(e) - } -}, 1000) +await client.getBlockNumber() +await webSocketClient.getBlockNumber() + +document.getElementById('app')!.innerText = 'success' From 72fad5b574946af91cd0cbb99e7c50150bfbc4b3 Mon Sep 17 00:00:00 2001 From: alvarius Date: Wed, 16 Jul 2025 17:56:25 +0200 Subject: [PATCH 8/9] Create lazy-turkeys-burn.md --- .changeset/lazy-turkeys-burn.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lazy-turkeys-burn.md diff --git a/.changeset/lazy-turkeys-burn.md b/.changeset/lazy-turkeys-burn.md new file mode 100644 index 0000000000..9263cf679b --- /dev/null +++ b/.changeset/lazy-turkeys-burn.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +Fixed reconnection logic for WebSocket clients. From 51b301c20d311964ebc4235fe6e85e396bc3e049 Mon Sep 17 00:00:00 2001 From: alvarius Date: Wed, 16 Jul 2025 18:58:41 +0200 Subject: [PATCH 9/9] self-review --- src/utils/rpc/socket.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/utils/rpc/socket.ts b/src/utils/rpc/socket.ts index 8057968c9a..0d5a4144c7 100644 --- a/src/utils/rpc/socket.ts +++ b/src/utils/rpc/socket.ts @@ -132,19 +132,22 @@ export async function getSocketRpcClient( let socket: Socket<{}> let keepAliveTimer: ReturnType | undefined - let reconnectLock = false + let reconnectInProgress = false function attemptReconnect() { + // Attempt to reconnect. if (reconnect && reconnectCount < attempts) { - if (reconnectLock) return - reconnectLock = true + if (reconnectInProgress) return + reconnectInProgress = true reconnectCount++ // Make sure the previous socket is definitely closed. socket?.close() setTimeout(async () => { await setup().catch(console.error) - reconnectLock = false + reconnectInProgress = false }, delay) - } else { + } + // Otherwise, clear all requests and subscriptions. + else { requests.clear() subscriptions.clear() }