UNPKG

viem

Version:

TypeScript Interface for Ethereum

153 lines 7.16 kB
import { SocketClosedError, TimeoutError } from '../../errors/request.js'; import { createBatchScheduler, } from '../promise/createBatchScheduler.js'; import { withTimeout } from '../promise/withTimeout.js'; import { idCache } from './id.js'; export const socketClientCache = /*#__PURE__*/ new Map(); export async function getSocketRpcClient(parameters) { const { getSocket, keepAlive = true, key = 'socket', reconnect = true, url, } = parameters; const { interval: keepAliveInterval = 30_000 } = typeof keepAlive === 'object' ? keepAlive : {}; const { attempts = 5, delay = 2_000 } = typeof reconnect === 'object' ? reconnect : {}; let socketClient = socketClientCache.get(`${key}:${url}`); // If the socket already exists, return it. if (socketClient) return socketClient; let reconnectCount = 0; const { schedule } = createBatchScheduler({ id: `${key}:${url}`, fn: async () => { // Set up a cache for incoming "synchronous" requests. const requests = new Map(); // Set up a cache for subscriptions (eth_subscribe). const subscriptions = new Map(); let error; let socket; let keepAliveTimer; // Set up socket implementation. async function setup() { 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 })); // Clear all requests and subscriptions. requests.clear(); subscriptions.clear(); // Attempt to reconnect. if (reconnect && reconnectCount < attempts) setTimeout(async () => { reconnectCount++; await setup().catch(console.error); }, delay); }, 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); // Clear all requests and subscriptions. requests.clear(); subscriptions.clear(); // Make sure socket is definitely closed. socketClient?.close(); // Attempt to reconnect. if (reconnect && reconnectCount < attempts) setTimeout(async () => { reconnectCount++; await setup().catch(console.error); }, delay); }, 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); } return result; } await setup(); error = undefined; // Create a new socket instance. socketClient = { close() { keepAliveTimer && clearInterval(keepAliveTimer); socket.close(); socketClientCache.delete(`${key}:${url}`); }, get socket() { return socket; }, request({ body, onError, onResponse }) { if (error && onError) onError(error); const id = body.id ?? idCache.take(); const callback = (response) => { if (typeof response.id === 'number' && id !== response.id) return; // If we are subscribing to a topic, we want to set up a listener for incoming // messages. if (body.method === 'eth_subscribe' && typeof response.result === 'string') subscriptions.set(response.result, { onResponse: callback, onError, }); // If we are unsubscribing from a topic, we want to remove the listener. if (body.method === 'eth_unsubscribe') subscriptions.delete(body.params?.[0]); onResponse(response); }; requests.set(id, { onResponse: callback, onError }); try { socket.request({ body: { jsonrpc: '2.0', id, ...body, }, }); } catch (error) { onError?.(error); } }, requestAsync({ body, timeout = 10_000 }) { return withTimeout(() => new Promise((onResponse, onError) => this.request({ body, onError, onResponse, })), { errorInstance: new TimeoutError({ body, url }), timeout, }); }, requests, subscriptions, url, }; socketClientCache.set(`${key}:${url}`, socketClient); return [socketClient]; }, }); const [_, [socketClient_]] = await schedule(); return socketClient_; } //# sourceMappingURL=socket.js.map