UNPKG

rx-nostr

Version:

A library based on RxJS, which allows Nostr applications to easily communicate with relays.

295 lines (249 loc) 7.23 kB
import * as Nostr from "nostr-typedef"; import { combineLatest, map, Observable } from "rxjs"; import type { Authenticator, ConnectionStrategy, FilledRxNostrConfig, } from "../config/index.js"; import { RxNostrAlreadyDisposedError } from "../error.js"; import { ConnectionState, ConnectionStatePacket, ErrorPacket, EventPacket, LazyREQ, MessagePacket, OkPacketAgainstEvent, OutgoingMessagePacket, } from "../packet.js"; import { fill } from "../utils/config.js"; import { normalizeRelayUrl } from "../utils/normalize-url.js"; import { AuthProxy } from "./auth.js"; import { PublishProxy } from "./publish.js"; import { RelayConnection, WebSocketCloseCode } from "./relay.js"; import { FinPacket, SubscribeProxy } from "./subscribe.js"; export interface SubscribeOptions { overwrite: boolean; autoclose: boolean; mode: REQMode; } /** * - `"default"`: Subscriptions are active only while the relay is marked as a default relay. * - `"temporary"`: Subscriptions are always active. */ export type REQMode = "default" | "temporary"; export class NostrConnection { private relay: RelayConnection; private pubProxy: PublishProxy; private subProxy: SubscribeProxy; private defaultSubscriptionIds: Set<string> = new Set(); private communicating = false; private strategy: ConnectionStrategy = "lazy"; private disconnectTimeout: number; private disconnectTimer?: ReturnType<typeof setTimeout>; private isDefaultRelay = false; private disposed = false; private _url: string; get url() { return this._url; } constructor(url: string, config: FilledRxNostrConfig) { this._url = normalizeRelayUrl(url); const authenticator = getAuthenticator(url, config); const relay = new RelayConnection(this.url, config); const authProxy = authenticator ? new AuthProxy({ relay, config, authenticator }) : null; const pubProxy = new PublishProxy({ relay, authProxy }); const subProxy = new SubscribeProxy({ relay, authProxy, config }); this.relay = relay; this.pubProxy = pubProxy; this.subProxy = subProxy; this.disconnectTimeout = config.disconnectTimeout; // Idling cold sockets combineLatest([ this.pubProxy.getLogicalConnectionSizeObservable(), this.subProxy.getLogicalConnectionSizeObservable(), ]) .pipe(map(([pubConns, subConns]) => pubConns + subConns)) .subscribe((logicalConns) => { this.communicating = logicalConns > 0; this.resetConnection(); }); } setConnectionStrategy(strategy: ConnectionStrategy): void { if (this.disposed) { return; } this.strategy = strategy; this.resetConnection(); } private resetConnection() { let strategy = this.strategy; if (!this.isDefaultRelay) { strategy = "lazy"; } switch (strategy) { case "lazy": { const disconnect = () => { if (!this.communicating) { this.relay.disconnect(WebSocketCloseCode.RX_NOSTR_IDLE); } }; if (this.disconnectTimeout > 0) { // clear existing timer if (this.disconnectTimeout) { clearTimeout(this.disconnectTimer); this.disconnectTimer = undefined; } // create a new timer this.disconnectTimer = setTimeout(disconnect, this.disconnectTimeout); } else disconnect(); break; } case "lazy-keep": { break; } case "aggressive": { if ( this.connectionState === "initialized" || this.connectionState === "dormant" ) { this.relay.connectManually(); } break; } } } markAsDefault(flag: boolean): void { if (this.disposed) { return; } this.isDefaultRelay = flag; if (!this.isDefaultRelay) { for (const subId of this.defaultSubscriptionIds) { this.subProxy.unsubscribe(subId); } this.defaultSubscriptionIds.clear(); } this.resetConnection(); } async publish(event: Nostr.Event<number>): Promise<void> { if (this.disposed) { return; } return this.pubProxy.publish(event); } confirmOK(eventId: string): void { if (this.disposed) { return; } this.pubProxy.confirmOK(eventId); } subscribe(req: LazyREQ, options?: Partial<SubscribeOptions>): void { if (this.disposed) { return; } const { mode, overwrite, autoclose } = fill(options ?? {}, { overwrite: false, autoclose: false, mode: "default", }); const [, subId] = req; if (mode === "default" && !this.isDefaultRelay) { return; } if (!overwrite && this.subProxy.isOngoingOrQueued(subId)) { return; } if (mode === "default") { this.defaultSubscriptionIds.add(subId); } this.subProxy.subscribe(req, autoclose); } unsubscribe(subId: string): void { if (this.disposed) { return; } this.defaultSubscriptionIds.delete(subId); this.subProxy.unsubscribe(subId); } getEventObservable(): Observable<EventPacket> { if (this.disposed) { throw new RxNostrAlreadyDisposedError(); } return this.subProxy.getEventObservable(); } getFinObservable(): Observable<FinPacket> { if (this.disposed) { throw new RxNostrAlreadyDisposedError(); } return this.subProxy.getFinObservable(); } getOkAgainstEventObservable(): Observable<OkPacketAgainstEvent> { if (this.disposed) { throw new RxNostrAlreadyDisposedError(); } return this.pubProxy.getOkAgainstEventObservable(); } getAllMessageObservable(): Observable<MessagePacket> { if (this.disposed) { throw new RxNostrAlreadyDisposedError(); } return this.relay.getAllMessageObservable(); } getOutgoingMessageObservable(): Observable<OutgoingMessagePacket> { if (this.disposed) { throw new RxNostrAlreadyDisposedError(); } return this.relay.getOutgoingMessageObservable(); } getConnectionStateObservable(): Observable<ConnectionStatePacket> { if (this.disposed) { throw new RxNostrAlreadyDisposedError(); } return this.relay.getConnectionStateObservable(); } get connectionState(): ConnectionState { return this.relay.state; } getErrorObservable(): Observable<ErrorPacket> { if (this.disposed) { throw new RxNostrAlreadyDisposedError(); } return this.relay.getErrorObservable().pipe( map((reason) => ({ from: this.url, reason, })), ); } connectManually() { this.relay.connectManually(); } dispose() { this[Symbol.dispose](); } [Symbol.dispose](): void { if (this.disposed) { return; } this.disposed = true; if (this.disconnectTimer) clearTimeout(this.disconnectTimer); this.disconnectTimer = undefined; this.relay.dispose(); this.pubProxy.dispose(); this.subProxy.dispose(); } } function getAuthenticator( url: string, config: FilledRxNostrConfig, ): Authenticator | undefined { const a = config.authenticator; if (!a) { return; } const c = a instanceof Function ? a(url) : a; return c === "auto" ? {} : c; }