UNPKG

nostr-relaypool

Version:

A Nostr RelayPool implementation in TypeScript using only nostr-tools library as a dependency.

187 lines (178 loc) 5.5 kB
import type {Filter, Event} from "nostr-tools"; import {mergeSimilarAndRemoveEmptyFilters} from "./merge-similar-filters"; import { doNotEmitDuplicateEvents, doNotEmitOlderEvents, matchOnEventFilters, type OnEvent, } from "./on-event-filters"; import type {EventCache} from "./event-cache"; import type {FilterToSubscribe} from "./relay-pool"; import {CallbackReplayer} from "./callback-replayer"; const unique = (arr: string[]) => [...new Set(arr)]; export function groupFiltersByRelayAndEmitCacheHits( filters: (Filter & {relay?: string; noCache?: boolean})[], relays: string[], onEvent: OnEvent, options: { allowDuplicateEvents?: boolean; allowOlderEvents?: boolean; logAllEvents?: boolean; } = {}, eventCache?: EventCache ): [OnEvent, Map<string, Filter[]>] { let events: Event[] = []; if (eventCache) { const cachedEventsWithUpdatedFilters = eventCache.getCachedEventsWithUpdatedFilters(filters, relays); filters = cachedEventsWithUpdatedFilters.filters; events = cachedEventsWithUpdatedFilters.events; } if (options.logAllEvents) { const onEventNow = onEvent; // Nasty bug without introducing a new variable onEvent = (event, afterEose, url) => { onEventNow(event, afterEose, url); }; } if (!options.allowDuplicateEvents) { onEvent = doNotEmitDuplicateEvents(onEvent); } if (!options.allowOlderEvents) { onEvent = doNotEmitOlderEvents(onEvent); } for (const event of events) { onEvent(event, false, undefined); } filters = mergeSimilarAndRemoveEmptyFilters(filters); onEvent = matchOnEventFilters(onEvent, filters); relays = unique(relays); const filtersByRelay = getFiltersByRelay(filters, relays); return [onEvent, filtersByRelay]; } function getFiltersByRelay( filters: (Filter & {relay?: string})[], relays: string[] ): Map<string, Filter[]> { const filtersByRelay = new Map<string, Filter[]>(); const filtersWithoutRelay: Filter[] = []; for (const filter of filters) { const relay = filter.relay; if (relay) { const relayFilters = filtersByRelay.get(relay); if (relayFilters) { relayFilters.push(withoutRelay(filter)); } else { filtersByRelay.set(relay, [withoutRelay(filter)]); } } else { filtersWithoutRelay.push(filter); } } if (filtersWithoutRelay.length > 0) { for (const relay of relays) { const filters = filtersByRelay.get(relay); if (filters) { filtersByRelay.set(relay, filters.concat(filtersWithoutRelay)); } else { filtersByRelay.set(relay, filtersWithoutRelay); } } } return filtersByRelay; } function withoutRelay(filter: Filter & {relay?: string}): Filter { filter = {...filter}; delete filter.relay; return filter; } export function batchFiltersByRelay( subscribedFilters: FilterToSubscribe[], subscriptionCache?: Map< string, CallbackReplayer<[Event, boolean, string | undefined], OnEvent> > ): [OnEvent, Map<string, Filter[]>, {unsubcb?: () => void}] { const filtersByRelay = new Map<string, Filter[]>(); const onEvents: OnEvent[] = []; let counter = 0; let unsubOnEoseCounter = 0; let allUnsub = {unsubcb: () => {}, unsuboneosecb: () => {}}; let unsubVirtualSubscription = () => { counter--; if (counter === 0) { allUnsub.unsubcb(); } else if (unsubOnEoseCounter === 0) { allUnsub.unsuboneosecb(); } }; for (const [ onEvent, filtersByRelayBySub, unsub, unsubscribeOnEose, subscriptionCacheKey, ] of subscribedFilters) { if (!unsub.unsubcb) { continue; } for (const [relay, filters] of filtersByRelayBySub) { const filtersByRelayFilters = filtersByRelay.get(relay); if (filtersByRelayFilters) { filtersByRelay.set(relay, filtersByRelayFilters.concat(filters)); } else { filtersByRelay.set(relay, filters); } } let onEventWithUnsub: OnEvent = (event, afterEose, url) => { if (unsub.unsubcb) { onEvent(event, afterEose, url); } }; if (subscriptionCache && subscriptionCacheKey) { const callbackReplayer: CallbackReplayer< [Event, boolean, string | undefined], OnEvent > = new CallbackReplayer(unsubVirtualSubscription); onEvents.push((event, afterEose, url) => { callbackReplayer.event(event, afterEose, url); }); let unsubReplayerVirtualSubscription = callbackReplayer.sub(onEventWithUnsub); subscriptionCache.set(subscriptionCacheKey, callbackReplayer); unsub.unsubcb = () => { unsub.unsubcb = undefined; unsubReplayerVirtualSubscription(); if (!unsubscribeOnEose) { unsubOnEoseCounter--; } }; } else { onEvents.push(onEventWithUnsub); unsub.unsubcb = () => { unsub.unsubcb = undefined; unsubVirtualSubscription(); if (!unsubscribeOnEose) { unsubOnEoseCounter--; } }; } counter++; if (!unsubscribeOnEose) { unsubOnEoseCounter++; } } if (unsubOnEoseCounter === 0) { setTimeout(() => { allUnsub.unsuboneosecb(); }, 0); } else { // console.log("NO unsuboneosecb for ", subscribedFilters); } const onEvent: OnEvent = (event, afterEose, url) => { for (const onEvent of onEvents) { onEvent(event, afterEose, url); } }; subscribedFilters.length = 0; return [onEvent, filtersByRelay, allUnsub]; }