Bpistle/src/services/poll-results.service.ts
codytseng f480c9e080 fix: 🐛
2025-07-27 23:14:29 +08:00

196 lines
5.4 KiB
TypeScript

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<string, Set<string>>
voters: Set<string>
updatedAt: number
}
type TFetchPollResultsParams = {
pollEventId: string
relays: string[]
validPollOptionIds: string[]
isMultipleChoice: boolean
endsAt?: number
}
class PollResultsService {
static instance: PollResultsService
private pollResultsMap: Map<string, TPollResults> = new Map()
private pollResultsSubscribers = new Map<string, Set<() => void>>()
private loader = new DataLoader<TFetchPollResultsParams, TPollResults | undefined>(
async (params) => {
const pollMap = new Map<string, Omit<TFetchPollResultsParams, 'pollEventId'>>()
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<string, TPollResults>()
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<string>()
return acc
},
{} as Record<string, Set<string>>
),
voters: new Set<string>(),
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<typeof response> => 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