UNPKG

@msquared/etherbase-client

Version:

React hooks for interacting with Etherbase smart contracts

344 lines (343 loc) 13 kB
import { getConfig } from "../config"; export class WebSocketManager { static get() { if (!WebSocketManager.instance) { WebSocketManager.instance = new WebSocketManager(); } return WebSocketManager.instance; } constructor() { this.ws = null; this.dead = false; /** A map from localId -> SubscriptionStatus. */ this.subscriptions = new Map(); this.writerWs = null; this.readerWs = null; const config = getConfig(); this.readerUrl = config.wsReaderUrl; this.writerUrl = config.wsWriterUrl; this.privateKey = config.privateKey; } /** Connect if not open or connecting */ async connectReader() { if (this.dead) return; if (!this.readerUrl) { console.error("[WebSocketManager] Reader URL not set"); return; } if (this.readerWs && (this.readerWs.readyState === WebSocket.OPEN || this.readerWs.readyState === WebSocket.CONNECTING)) { if (this.readerWs.readyState === WebSocket.CONNECTING) { await new Promise((resolve) => { if (!this.readerWs) return; this.readerWs.addEventListener("open", () => resolve(), { once: true, }); }); } return; } console.log("[WebSocketManager] Connecting reader..."); const url = new URL(`${this.readerUrl}/read`); if (this.privateKey) { url.searchParams.set("privateKey", this.privateKey); } this.readerWs = new WebSocket(url.toString()); this.readerWs.onopen = () => { console.log("[WebSocketManager] Reader connected."); // Re-subscribe all on reconnect for (const status of this.subscriptions.values()) { status.subscriptionId = undefined; status.error = undefined; this.sendSubscribe(status); } }; this.readerWs.onmessage = (event) => this.handleMessage(event); this.readerWs.onerror = (err) => { console.error("[WebSocketManager] Reader WebSocket error:", err); }; this.readerWs.onclose = () => { console.warn("[WebSocketManager] Reader WebSocket closed."); this.readerWs = null; }; await new Promise((resolve) => { if (this.readerWs?.readyState === WebSocket.OPEN) { resolve(); } else { this.readerWs?.addEventListener("open", () => resolve(), { once: true }); } }); console.log("[WebSocketManager] Reader connected."); } async connectWriter() { if (this.dead) return; if (!this.writerUrl) { console.error("[WebSocketManager] Writer URL not set"); return; } if (this.writerWs && (this.writerWs.readyState === WebSocket.OPEN || this.writerWs.readyState === WebSocket.CONNECTING)) { if (this.writerWs.readyState === WebSocket.CONNECTING) { await new Promise((resolve) => { if (!this.writerWs) return; this.writerWs.addEventListener("open", () => resolve(), { once: true, }); }); } return; } console.log("[WebSocketManager] Connecting writer..."); const url = new URL(`${this.writerUrl}/write`); if (this.privateKey) { url.searchParams.set("privateKey", this.privateKey); } this.writerWs = new WebSocket(url.toString()); this.writerWs.onopen = () => { console.log("[WebSocketManager] Writer connected."); }; this.writerWs.onmessage = (event) => this.handleWriterMessage(event); this.writerWs.onerror = (err) => { console.error("[WebSocketManager] Writer WebSocket error:", err); }; this.writerWs.onclose = () => { console.warn("[WebSocketManager] Writer WebSocket closed."); this.writerWs = null; }; await new Promise((resolve) => { if (this.writerWs?.readyState === WebSocket.OPEN) { resolve(); } else { this.writerWs?.addEventListener("open", () => resolve(), { once: true }); } }); console.log("[WebSocketManager] Writer connected."); } handleMessage(event) { try { const msg = JSON.parse(event.data); switch (msg.type) { case "error": console.error("[WS] Server error:", msg.error); break; case "subscription_success": { const { pendingId, subscriptionId } = msg.data; const sub = this.subscriptions.get(pendingId); if (sub) { sub.subscriptionId = subscriptionId; sub.error = undefined; console.log(`[WS] Subscription success for localId=${pendingId}, subscriptionId=${subscriptionId}`); } break; } case "subscription_failed": { const { pendingId, error } = msg.data; const sub = this.subscriptions.get(pendingId); if (sub) { sub.error = error; console.warn(`[WS] Subscription failed for localId=${pendingId}: ${error}`); } break; } case "updates": { console.log("[WS] Got updates:", msg.data); this.handleUpdates(msg.data); break; } case "initial_state": { this.handleUpdates(msg.data); break; } case "transaction_complete": console.log("[WS] transaction_complete:", msg.data); break; case "transaction_failed": console.warn("[WS] transaction_failed:", msg.data); break; case "event_submitted": console.log("[WS] event_submitted"); break; default: console.log("[WS] unhandled message:", msg); break; } } catch (err) { console.error("[WS] Failed to parse message:", err); } } handleWriterMessage(event) { try { const msg = JSON.parse(event.data); switch (msg.type) { case "error": console.error("[WS Writer] Server error:", msg.error); break; case "transaction_complete": console.log("[WS Writer] transaction_complete:", msg.data); break; case "transaction_failed": console.warn("[WS Writer] transaction_failed:", msg.data); break; case "event_submitted": console.log("[WS Writer] event_submitted"); break; default: console.log("[WS Writer] unhandled message:", msg); break; } } catch (err) { console.error("[WS Writer] Failed to parse message:", err); } } handleUpdates({ block, updates }) { for (const update of updates) { // The server includes a subscriptionId. Find a local subscription that matches. const sub = [...this.subscriptions.values()].find((s) => s.subscriptionId === update.subscriptionId); if (!sub) { console.warn("[WS] Got update for unknown subscriptionId:", update.subscriptionId); continue; } if (update.type === "event" && "onEvent" in sub.subscription) { sub.subscription.onEvent(update.data); } else if (update.type === "state" && "onStateUpdate" in sub.subscription) { sub.subscription.onStateUpdate(update.data); } } } async addEventSubscription(localId, subscription) { this.subscriptions.set(localId, { localId, subscription, }); await this.connectReader(); const status = this.subscriptions.get(localId); if (!status) { console.warn("[WS] Tried to add event subscription for unknown localId:", localId); return; } this.sendSubscribe(status); } async addStateSubscription(localId, subscription) { this.subscriptions.set(localId, { localId, subscription, }); console.log("Adding state subscription", localId, subscription); await this.connectReader(); const status = this.subscriptions.get(localId); if (!status) { console.warn("[WS] Tried to add state subscription for unknown localId:", localId); return; } this.sendSubscribe(status); } removeSubscription(localId) { const status = this.subscriptions.get(localId); if (!status) return; // If we already have a server subscriptionId, unsubscribe from the server if (status.subscriptionId && this.readerWs?.readyState === WebSocket.OPEN) { this.readerWs.send(JSON.stringify({ type: "unsubscribe", data: { subscriptionId: status.subscriptionId, }, })); } this.subscriptions.delete(localId); } sendSubscribe(status) { if (!this.readerWs || this.readerWs.readyState !== WebSocket.OPEN) return; const { localId, subscription } = status; if ("onEvent" in subscription) { this.readerWs.send(JSON.stringify({ type: "subscribe", data: { pendingId: localId, eventSubscription: { contractAddress: subscription.contractAddress, events: subscription.events, }, }, })); } else { console.log("Sending state subscription!!!", localId, subscription); this.readerWs.send(JSON.stringify({ type: "subscribe", data: { pendingId: localId, stateSubscription: { contractAddress: subscription.contractAddress, statePath: subscription.statePath, options: subscription.options, }, }, })); } } getSubscriptionError(localId) { return this.subscriptions.get(localId)?.error; } forceClose() { console.log("[WebSocketManager] Force closing..."); this.dead = true; if (this.readerWs) { this.readerWs.close(); this.readerWs = null; } if (this.writerWs) { this.writerWs.close(); this.writerWs = null; } } async setValue({ contractAddress, state, }) { if (!this.writerWs || this.writerWs.readyState !== WebSocket.OPEN) { await this.connectWriter(); } this.writerWs?.send(JSON.stringify({ type: "set_value", data: { contractAddress, state, }, })); } async emitEvent({ sourceAddress, name, args, }) { console.log("[WebSocketManager] Emitting event...", this.writerWs?.readyState); if (!this.writerWs || this.writerWs.readyState !== WebSocket.OPEN) { await this.connectWriter(); } this.writerWs?.send(JSON.stringify({ type: "emit_event", data: { contractAddress: sourceAddress, name, args, }, })); } async executeContractMethod({ contractAddress, methodName, args, }) { console.log("[WebSocketManager] Executing contract method...", this.writerWs?.readyState); if (!this.writerWs || this.writerWs.readyState !== WebSocket.OPEN) { await this.connectWriter(); } this.writerWs?.send(JSON.stringify({ type: "execute_contract_method", data: { contractAddress, methodName, args }, })); } }