-
-
Notifications
You must be signed in to change notification settings - Fork 838
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Websocket reconnect doesn't work #2563
Comments
Hooked up |
thank you @jxom |
@justefg I suppose you are using chainstack as your node provider? /**
* NOTE: We are using a custom re-connect mechanism as node providers
* might close the websocket even if keep-alives are used. Viem does not
* support this reconnect mechanism out of the box, so we have to implement
* this ourselves.
*/
private async connect() {
this._logger.info("Connecting...");
const socketRpcClient = await this._client.transport.getRpcClient();
const heartbeat = () => {
this._logger.info("ping ->");
this._client
.getBlockNumber()
.then((_) => {
this._logger.info("<- pong");
})
.catch((err) => this._logger.error(err));
};
const intervalId = setInterval(heartbeat, 5 * 60 * 1000);
const onOpen = (_: Event) => {
// drop
};
const onMessage = (_: MessageEvent<any>) => {
// drop
};
const onError = (ev: Event) => {
this._logger.error(ev);
};
const onClose = async () => {
this._logger.warn("Websocket connection closed!");
socketRpcClient.socket.removeEventListener("open", onOpen);
socketRpcClient.socket.removeEventListener("message", onMessage);
socketRpcClient.socket.removeEventListener("error", onError);
socketRpcClient.socket.removeEventListener("close", onClose);
// NOTE: IMPORTANT: invalidate viem's socketClientCache! When close
// happens on socket level, the same socketClient with the closed websocket will be
// re-used from cache leading to 'Socket is closed.' error.
socketRpcClient.close();
clearInterval(intervalId);
this._client = this._clientManager.getClient(this._chain);
this._logger.info("Re-establishing connection!");
this.connect();
};
const setupEventListeners = () => {
socketRpcClient.socket.addEventListener("open", onOpen);
socketRpcClient.socket.addEventListener("message", onMessage);
socketRpcClient.socket.addEventListener("error", onError);
socketRpcClient.socket.addEventListener("close", onClose);
};
setupEventListeners();
heartbeat();
} |
Hey @jxom, hows progress on that? Just run into the issue today. Is is already fixed in latest release? |
@nstylo interesting. have you tested it? |
@justefg yes, works very reliably. I have to see on which version of view I am using this solution. |
Hey @nstylo Have anyone else also faced a similar issue? Or am i doing something wrong here? export class WsListener {
private logger = new Logger(WsListener.name);
private client: PublicClient;
private contractAddress;
private eventAbi;
private handlerFn;
private chainId;
private wsUrl;
private intervalId: NodeJS.Timeout;
private connectionEventTracker;
private eventName;
constructor({
chainId,
wsUrl,
contractAddress,
eventName,
eventAbi,
handlerFn,
connectionEventTracker,
}: {
chainId: number;
wsUrl: string;
contractAddress: `0x${string}`;
eventName: string;
eventAbi: string;
handlerFn: (chainId: number, log: EVENT_LOG[]) => any;
connectionEventTracker: (params: {
chainId: number;
trackedEvent: string;
type: WebsocketConnectionEventType;
}) => Promise<void>;
}) {
this.wsUrl = wsUrl;
this.contractAddress = contractAddress;
this.eventName = eventName;
this.eventAbi = eventAbi;
this.handlerFn = handlerFn;
this.chainId = chainId;
this.connectionEventTracker = connectionEventTracker;
this.connectClient();
}
private async connectClient() {
try {
// add a log in db, when a new connection is created
this.connectionEventTracker({
chainId: this.chainId,
trackedEvent: this.eventName,
type: WebsocketConnectionEventType.Connection,
});
this.client = createPublicClient({
transport: webSocket(this.wsUrl, {
keepAlive: { interval: 1_000 }, // 1000 ms (will send keep alive ping messages every 1 sec)
reconnect: true,
retryCount: 5,
timeout: 60_000, // 60 secs
}),
});
this.attachHandler();
await this.setupEventListeners();
// ping every min
this.intervalId = setInterval(() => this.heartbeat(), 1 * 60 * 1000);
} catch (err) {
this.logger.error(`Error while connecting client: `, err);
}
}
private attachHandler() {
// listen to event
this.client.watchContractEvent({
address: this.contractAddress,
abi: parseAbi([this.eventAbi]),
eventName: this.eventName,
onLogs: async (logs: EVENT_LOG[]) => {
this.logger.log(
`...received event ${this.eventName} on ${this.chainId}...`,
);
await this.handlerFn(this.chainId, logs);
},
});
}
private async setupEventListeners() {
const socketRpcClient = await this.client.transport.getRpcClient();
// using arrow function wrappers over the event listeners, to preserve `this` context
socketRpcClient.socket.addEventListener('open', (args: any) =>
this.onOpen(args),
);
socketRpcClient.socket.addEventListener('message', (args: any) =>
this.onMessage(args),
);
socketRpcClient.socket.addEventListener('error', (args: any) =>
this.onError(args),
);
socketRpcClient.socket.addEventListener('close', () => this.onClose());
}
getClient() {
return this.client;
}
async heartbeat() {
try {
this.logger.log(`...ping.........`);
await this.client.getBlockNumber();
this.logger.log(`.........pong...`);
} catch (err) {
this.logger.error(`---heartbeat-error---`, err);
throw err;
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onOpen(_: Event) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onMessage(_: MessageEvent<any>) {}
onError(ev: Event) {
this.logger.error(`websocket error: `, ev);
}
async onClose() {
try {
this.logger.warn('Websocket connection closed!');
const socketRpcClient = await this.client.transport.getRpcClient();
socketRpcClient.socket.removeEventListener('open', (args: any) =>
this.onOpen(args),
);
socketRpcClient.socket.removeEventListener('message', (args: any) =>
this.onMessage(args),
);
socketRpcClient.socket.removeEventListener('error', (args: any) =>
this.onError(args),
);
socketRpcClient.socket.removeEventListener('close', () => this.onClose());
// NOTE: IMPORTANT: invalidate viem's socketClientCache! When close
// happens on socket level, the same socketClient with the closed websocket will be
// re-used from cache leading to 'Socket is closed.' error.
socketRpcClient.close();
// logs in db, when a disconnection happens
this.connectionEventTracker({
chainId: this.chainId,
trackedEvent: this.eventName,
type: WebsocketConnectionEventType.Disconnection,
});
clearInterval(this.intervalId);
this.logger.log('....Re-establishing connection!..');
this.connectClient();
this.logger.log('....Re-established connection!..');
} catch (err) {
this.logger.error(`Error while closing connection: `, err);
}
}
} |
@arpan-jain have you checked websockets/ws#1869? what does |
Yes. netstat shows me new connections with |
@jxom can you assist please? |
@arpan-jain @jxom it looks like we should add |
How I would go about making sure reconnections work properly? You can use a firewall for that.
then once it attempts reconnecting you can comment the line and reload again
On linux you can use iptables or ufw. |
Check existing issues
Viem Version
2.18.5
Current Behavior
Websocket doesn't reconnect after a connection is dropped preventing users from receiving events.
Expected Behavior
Steps To Reproduce
Run minimal reproducible example using
ts-node
:ts-node example.ts
It works fine for about an hour but then after a connection drop events are no longer received. Tested with multiple websocket providers.
Link to Minimal Reproducible Example
https://gist.github.com/justefg/95acdcb5d8cbee6930b6177120c24cbc
Anything else?
No response
The text was updated successfully, but these errors were encountered: