UNPKG

rx-nostr

Version:

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

285 lines (241 loc) 6.79 kB
import * as Nostr from "nostr-typedef"; import { filter, Observable, Subject } from "rxjs"; import type { FilledRxNostrConfig } from "../config/index.js"; import { evalFilters } from "../lazy-filter.js"; import { Nip11Registry } from "../nip11.js"; import { isFiltered } from "../nostr/filter.js"; import { EventPacket, LazyREQ } from "../packet.js"; import { AuthProxy } from "./auth.js"; import { RelayConnection } from "./relay.js"; import { CounterSubject } from "./utils.js"; export interface FinPacket { from: string; subId: string; } export class SubscribeProxy { // maxSubscriptions: number | null = undefined; // maxFilters: number | null; // maxLimit: number | null; private relay: RelayConnection; private authProxy: AuthProxy | null; private config: FilledRxNostrConfig; private subs = new Map<string, SubRecord>(); private authRequiredSubs = new Set<string>(); private fin$ = new Subject<FinPacket>(); private disposed = false; private queue: SubQueue; constructor(params: { relay: RelayConnection; authProxy: AuthProxy | null; config: FilledRxNostrConfig; }) { this.relay = params.relay; this.authProxy = params.authProxy; this.config = params.config; this.queue = new SubQueue(this.relay.url, this.config); // Dequeuing this.queue.getActivationObservable().subscribe((activated) => { for (const { req } of activated) { this.sendREQ(req); } }); // Recovering this.relay.getReconnectedObservable().subscribe(() => { for (const { req } of this.queue.ongoings) { this.sendREQ(req); } }); // Auto closing this.relay.getEOSEObservable().subscribe(({ subId }) => { if (this.subs.get(subId)?.autoclose) { this.unsubscribe(subId); } }); // Mark as closed this.relay.getCLOSEDObservable().subscribe(async ({ subId, notice }) => { const sub = this.subs.get(subId); if (!sub) { return; } if (this.authProxy && notice?.startsWith("auth-required:")) { this.authRequiredSubs.add(subId); } else { this.fin(subId); } }); this.authProxy?.getAuthResultObservable().subscribe((ok) => { if (ok) { for (const subId of this.authRequiredSubs) { const req = this.subs.get(subId)?.req; if (req) { this.sendREQ(req); } } } else { for (const subId of this.authRequiredSubs) { this.fin(subId); } } this.authRequiredSubs.clear(); }); } subscribe(req: LazyREQ, autoclose: boolean): void { if (this.disposed) { return; } const subId = req[1]; const sub: SubRecord = { subId, req, autoclose, }; this.subs.set(subId, sub); this.queue.enqueue(sub); } unsubscribe(subId: string): void { if (this.disposed) { return; } if (this.subs.has(subId)) { this.sendCLOSE(subId); } this.fin(subId); } isOngoingOrQueued(subId: string): boolean { return this.subs.has(subId); } getEventObservable(): Observable<EventPacket> { return this.relay.getEVENTObservable().pipe( filter(({ subId, event }) => { const filters = this.subs.get(subId)?.filters; if (!filters) { return false; } return ( this.config.skipValidateFilterMatching || isFiltered(event, filters) ); }), ); } getFinObservable(): Observable<FinPacket> { return this.fin$.asObservable(); } getLogicalConnectionSizeObservable(): Observable<number> { return this.queue.getSizeObservable(); } dispose() { this[Symbol.dispose](); } [Symbol.dispose](): void { if (this.disposed) { return; } this.disposed = true; const subjects = [this.fin$]; for (const sub of subjects) { sub.complete(); } this.queue.dispose(); } private sendREQ([, subId, ...lazyFilters]: LazyREQ) { const filters = evalFilters(lazyFilters); const sub = this.subs.get(subId); if (!sub) { return; } sub.filters = filters; this.relay.send(["REQ", subId, ...filters]); } private sendCLOSE(subId: string) { this.relay.send(["CLOSE", subId]); } private fin(subId: string) { this.subs.delete(subId); this.queue.drop(subId); this.fin$.next({ from: this.relay.url, subId, }); } } interface SubRecord { subId: string; req: LazyREQ; filters?: Nostr.Filter[]; autoclose: boolean; } class SubQueue { private _queuings: SubRecord[] = []; private _ongoings: SubRecord[] = []; private activated$ = new Subject<SubRecord[]>(); private count$ = new CounterSubject(); get queuings(): SubRecord[] { return this._queuings; } private set queuings(v: SubRecord[]) { this._queuings = v; } get ongoings(): SubRecord[] { return this._ongoings; } private set ongoings(v: SubRecord[]) { this._ongoings = v; } constructor( private url: string, private config: FilledRxNostrConfig, ) {} enqueue(v: SubRecord): void { this.queuings = [...this.queuings, v]; this.count$.increment(); this.shift(); } drop(subId: string): void { const remove = (arr: SubRecord[], subId: string): [SubRecord[], number] => { const prevLength = arr.length; const filtered = arr.filter((e) => e.subId !== subId); const removed = prevLength - filtered.length; return [filtered, removed]; }; const [queuings, droppedX] = remove(this.queuings, subId); const [ongoings, droppedY] = remove(this.ongoings, subId); this.queuings = queuings; this.ongoings = ongoings; this.count$.next((v) => v - (droppedX + droppedY)); this.shift(); } getActivationObservable() { return this.activated$.asObservable(); } getSizeObservable() { return this.count$.asObservable(); } dispose() { const subjects = [this.activated$, this.count$]; for (const sub of subjects) { sub.complete(); } } private async shift() { const capacity = await this.capacity(); const concated = [...this.ongoings, ...this.queuings]; const ongoings = concated.slice(0, capacity); const queuings = concated.slice(capacity); const activated = this.queuings.slice(0, capacity - this.ongoings.length); this.ongoings = ongoings; this.queuings = queuings; if (activated.length > 0) { this.activated$.next(activated); } } private async capacity() { const capacity = await Nip11Registry.getValue( this.url, (data) => data.limitation?.max_subscriptions, { skipFetch: this.config.skipFetchNip11, }, ); return capacity ?? Infinity; } }