import { ExtendedKind } from '@/constants' import { getPollResponseFromEvent } from '@/lib/event-metadata' import DataLoader from 'dataloader' import dayjs from 'dayjs' import { Filter } from 'nostr-tools' import client from './client.service' export type TPollResults = { totalVotes: number results: Record> voters: Set updatedAt: number } type TFetchPollResultsParams = { pollEventId: string relays: string[] validPollOptionIds: string[] isMultipleChoice: boolean endsAt?: number } class PollResultsService { static instance: PollResultsService private pollResultsMap: Map = new Map() private pollResultsSubscribers = new Map void>>() private loader = new DataLoader( async (params) => { const pollMap = new Map>() params.forEach(({ pollEventId, relays, validPollOptionIds, isMultipleChoice, endsAt }) => { if (!pollMap.has(pollEventId)) { pollMap.set(pollEventId, { relays, validPollOptionIds, isMultipleChoice, endsAt }) } }) const pollResults = await Promise.allSettled( Array.from(pollMap).map(async ([pollEventId, pollParams]) => { const result = await this._fetchResults( pollEventId, pollParams.relays, pollParams.validPollOptionIds, pollParams.isMultipleChoice, pollParams.endsAt ) return { pollEventId, result } }) ) const resultMap = new Map() pollResults.forEach((promiseResult) => { if (promiseResult.status === 'fulfilled' && promiseResult.value.result) { resultMap.set(promiseResult.value.pollEventId, promiseResult.value.result) } }) return params.map(({ pollEventId }) => resultMap.get(pollEventId)) }, { cache: false } ) constructor() { if (!PollResultsService.instance) { PollResultsService.instance = this } return PollResultsService.instance } async fetchResults( pollEventId: string, relays: string[], validPollOptionIds: string[], isMultipleChoice: boolean, endsAt?: number ) { return this.loader.load({ pollEventId, relays, validPollOptionIds, isMultipleChoice, endsAt }) } private async _fetchResults( pollEventId: string, relays: string[], validPollOptionIds: string[], isMultipleChoice: boolean, endsAt?: number ) { const filter: Filter = { kinds: [ExtendedKind.POLL_RESPONSE], '#e': [pollEventId], limit: 1000 } if (endsAt) { filter.until = endsAt } let results = this.pollResultsMap.get(pollEventId) if (results) { if (endsAt && results.updatedAt >= endsAt) { return results } filter.since = results.updatedAt } else { results = { totalVotes: 0, results: validPollOptionIds.reduce( (acc, optionId) => { acc[optionId] = new Set() return acc }, {} as Record> ), voters: new Set(), updatedAt: 0 } } const responseEvents = await client.fetchEvents(relays, filter) results.updatedAt = dayjs().unix() const responses = responseEvents .map((evt) => getPollResponseFromEvent(evt, validPollOptionIds, isMultipleChoice)) .filter((response): response is NonNullable => response !== null) responses .sort((a, b) => b.created_at - a.created_at) .forEach((response) => { if (results && results.voters.has(response.pubkey)) return results.voters.add(response.pubkey) results.totalVotes += response.selectedOptionIds.length response.selectedOptionIds.forEach((optionId) => { if (results.results[optionId]) { results.results[optionId].add(response.pubkey) } }) }) this.pollResultsMap.set(pollEventId, { ...results }) if (responseEvents.length) { this.notifyPollResults(pollEventId) } return results } subscribePollResults(pollEventId: string, callback: () => void) { let set = this.pollResultsSubscribers.get(pollEventId) if (!set) { set = new Set() this.pollResultsSubscribers.set(pollEventId, set) } set.add(callback) return () => { set?.delete(callback) if (set?.size === 0) this.pollResultsSubscribers.delete(pollEventId) } } private notifyPollResults(pollEventId: string) { const set = this.pollResultsSubscribers.get(pollEventId) if (set) { set.forEach((cb) => cb()) } } getPollResults(id: string): TPollResults | undefined { return this.pollResultsMap.get(id) } addPollResponse(pollEventId: string, pubkey: string, selectedOptionIds: string[]) { const results = this.pollResultsMap.get(pollEventId) if (!results) return if (results.voters.has(pubkey)) return results.voters.add(pubkey) results.totalVotes += selectedOptionIds.length selectedOptionIds.forEach((optionId) => { if (results.results[optionId]) { results.results[optionId].add(pubkey) } }) this.pollResultsMap.set(pollEventId, { ...results }) this.notifyPollResults(pollEventId) } } const instance = new PollResultsService() export default instance