import type { NostrEvent, ClientMessage, RelayMessage, Filter, Subscription, } from "./types"; export class Relay { private url: string; private ws: WebSocket | null = null; private subscriptions: Map = new Map(); private messageQueue: ClientMessage[] = []; private reconnectTimer: Timer | null = null; private reconnectAttempts = 0; private maxReconnectAttempts = 5; private reconnectDelay = 1000; public status: "connecting" | "connected" | "disconnected" | "error" = "disconnected"; public onconnect?: () => void; public ondisconnect?: () => void; public onerror?: (error: Error) => void; public onnotice?: (message: string) => void; constructor(url: string) { this.url = url; } async connect(): Promise { return new Promise((resolve, reject) => { if (this.ws?.readyState === WebSocket.OPEN) { resolve(); return; } this.status = "connecting"; this.ws = new WebSocket(this.url); this.ws.onopen = () => { this.status = "connected"; this.reconnectAttempts = 0; this.flushMessageQueue(); this.onconnect?.(); resolve(); }; this.ws.onclose = () => { this.status = "disconnected"; this.ondisconnect?.(); this.attemptReconnect(); }; this.ws.onerror = (event) => { this.status = "error"; const error = new Error(`WebSocket error: ${event.type}`); this.onerror?.(error); reject(error); }; this.ws.onmessage = (event) => { this.handleMessage(event.data); }; }); } disconnect(): void { if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } if (this.ws) { this.ws.close(); this.ws = null; } this.status = "disconnected"; this.subscriptions.clear(); this.messageQueue = []; } private attemptReconnect(): void { if (this.reconnectAttempts >= this.maxReconnectAttempts) { this.status = "error"; this.onerror?.(new Error("Max reconnection attempts reached")); return; } this.reconnectAttempts++; const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1); this.reconnectTimer = setTimeout(() => { this.connect().catch((error) => { console.error("Reconnection failed:", error); }); }, delay); } private flushMessageQueue(): void { while (this.messageQueue.length > 0) { const message = this.messageQueue.shift(); if (message) { this.send(message); } } } private send(message: ClientMessage): void { if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(message)); } else { this.messageQueue.push(message); } } private handleMessage(data: string): void { try { const message = JSON.parse(data) as RelayMessage; switch (message[0]) { case "EVENT": { const [, subscriptionId, event] = message; const subscription = this.subscriptions.get(subscriptionId); if (subscription) { subscription.onevent?.(event); } break; } case "OK": { const [, eventId, success, messag] = message; if (!success) { console.error(`Event ${eventId} rejected: ${messag}`); } break; } case "EOSE": { const [, subscriptionId] = message; const subscription = this.subscriptions.get(subscriptionId); subscription?.oneose?.(); break; } case "CLOSED": { const [, subscriptionId, messag] = message; this.subscriptions.delete(subscriptionId); console.log(`Subscription ${subscriptionId} closed: ${messag}`); break; } case "NOTICE": { const [, messag] = message; this.onnotice?.(messag); break; } case "AUTH": { console.warn("AUTH not implemented"); break; } } } catch (error) { console.error("Failed to handle message:", error); } } publishEvent(event: NostrEvent): void { this.send(["EVENT", event]); } subscribe( id: string, filters: Filter[], handlers: { onevent?: (event: NostrEvent) => void; oneose?: () => void; }, ): () => void { const subscription: Subscription = { id, filters, ...handlers, }; this.subscriptions.set(id, subscription); this.send(["REQ", id, ...filters]); return () => { this.unsubscribe(id); }; } unsubscribe(id: string): void { this.subscriptions.delete(id); this.send(["CLOSE", id]); } getStatus(): string { return this.status; } getUrl(): string { return this.url; } }