fix: switch fallback score calc to NIP-11 + NIP-50
This commit is contained in:
@@ -1,11 +1,26 @@
|
|||||||
import {request} from "@welshman/net"
|
import {request} from "@welshman/net"
|
||||||
import {clamp} from "@welshman/lib"
|
import {clamp} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import {nip19} from "nostr-tools"
|
||||||
|
import {getProfile} from "@welshman/app"
|
||||||
|
import type {Filter, TrustedEvent} from "@welshman/util"
|
||||||
|
|
||||||
const DEFAULT_ASSERTION_RELAYS =
|
const DEFAULT_EXTENDED_SEARCH_RELAYS = [
|
||||||
"wss://nip85.brainstorm.world,wss://nip85.nosfabrica.com,wss://nip85.uid.ovh"
|
import.meta.env.VITE_DEFAULT_SEARCH_RELAYS,
|
||||||
|
"relay.noswhere.com,search.nos.today,nostr.wine",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(",")
|
||||||
|
|
||||||
const toAssertionRelayUrl = (url: string) => {
|
const DEFAULT_SCORE = 50
|
||||||
|
const SEARCH_LIMIT = 40
|
||||||
|
const MAX_PROFILE_TERMS = 3
|
||||||
|
const MIN_PROFILE_TERM_LENGTH = 3
|
||||||
|
const REQUEST_THRESHOLD = 0.5
|
||||||
|
const PROFILE_LOOKUP_TIMEOUT_MS = 6_000
|
||||||
|
const RELAY_SEARCH_TIMEOUT_MS = 6_000
|
||||||
|
const NIP11_TIMEOUT_MS = 4_000
|
||||||
|
|
||||||
|
const toRelayUrl = (url: string) => {
|
||||||
const trimmed = url.trim()
|
const trimmed = url.trim()
|
||||||
|
|
||||||
if (!trimmed) return ""
|
if (!trimmed) return ""
|
||||||
@@ -16,81 +31,492 @@ const toAssertionRelayUrl = (url: string) => {
|
|||||||
return trimmed
|
return trimmed
|
||||||
}
|
}
|
||||||
|
|
||||||
const ASSERTION_RELAYS = String(import.meta.env.VITE_ASSERTION_RELAYS || DEFAULT_ASSERTION_RELAYS)
|
const toNip11Url = (url: string) => {
|
||||||
|
const normalized = toRelayUrl(url)
|
||||||
|
|
||||||
|
if (normalized.startsWith("wss://")) return normalized.replace(/^wss:/, "https:")
|
||||||
|
if (normalized.startsWith("ws://")) return normalized.replace(/^ws:/, "http:")
|
||||||
|
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
const EXTENDED_SEARCH_RELAYS = String(
|
||||||
|
import.meta.env.VITE_EXTENDED_SEARCH_RELAYS || DEFAULT_EXTENDED_SEARCH_RELAYS,
|
||||||
|
)
|
||||||
.split(",")
|
.split(",")
|
||||||
.map(toAssertionRelayUrl)
|
.map(toRelayUrl)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
.filter((url, index, urls) => urls.indexOf(url) === index)
|
||||||
|
|
||||||
const rankCache = new Map<string, number | null>()
|
type Nip11RelayInfo = {
|
||||||
const pendingRankRequests = new Map<string, Promise<number | null>>()
|
supportsNip50: boolean
|
||||||
|
nip50Extensions: string[]
|
||||||
const getMedian = (values: number[]) => {
|
|
||||||
const sorted = [...values].sort((a, b) => a - b)
|
|
||||||
const middle = Math.floor(sorted.length / 2)
|
|
||||||
|
|
||||||
if (sorted.length % 2 === 1) {
|
|
||||||
return sorted[middle]
|
|
||||||
}
|
|
||||||
|
|
||||||
return (sorted[middle - 1] + sorted[middle]) / 2
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRankFromEvent = (event: TrustedEvent) => {
|
type RelaySearchInfo = {
|
||||||
const rankTag = event.tags?.find((tag: string[]) => tag[0] === "rank")
|
relay: string
|
||||||
const rankValue = rankTag ? Number(rankTag[1]) : null
|
extensions: string[]
|
||||||
|
|
||||||
if (rankValue === null || !Number.isFinite(rankValue)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return clamp([0, 100], rankValue)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchPubkeyRank = async (pubkey: string): Promise<number | null> => {
|
type ScoreResult = {
|
||||||
if (ASSERTION_RELAYS.length === 0) {
|
score: number
|
||||||
return 50
|
index: number
|
||||||
|
relayHits: number
|
||||||
|
activeRelays: number
|
||||||
|
interleavedCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type TimedRequestOptions = {
|
||||||
|
relays: string[]
|
||||||
|
filters: Filter[]
|
||||||
|
timeout?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const rankCache = new Map<string, number>()
|
||||||
|
const pendingRankRequests = new Map<string, Promise<number>>()
|
||||||
|
const relayInfoCache = new Map<string, Promise<Nip11RelayInfo>>()
|
||||||
|
|
||||||
|
const toStringArray = (value: unknown): string[] => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value
|
||||||
|
.filter(item => typeof item === "string" || typeof item === "number")
|
||||||
|
.map(item => String(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return [value]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value && typeof value === "object") {
|
||||||
|
return Object.keys(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHexPubkey = (value: string) => /^[0-9a-f]{64}$/i.test(value)
|
||||||
|
|
||||||
|
const normalizePubkey = (value: string) => {
|
||||||
|
const normalized = value.trim().toLowerCase()
|
||||||
|
|
||||||
|
if (!normalized) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHexPubkey(normalized)) {
|
||||||
|
return normalized
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const events = await request({
|
const decoded = nip19.decode(normalized)
|
||||||
relays: ASSERTION_RELAYS,
|
|
||||||
autoClose: true,
|
|
||||||
filters: [{kinds: [30382], "#d": [pubkey], limit: 8}],
|
|
||||||
})
|
|
||||||
|
|
||||||
const ranks = events.map(getRankFromEvent).filter((rank): rank is number => rank !== null)
|
if (decoded.type === "npub" && typeof decoded.data === "string" && isHexPubkey(decoded.data)) {
|
||||||
|
return decoded.data.toLowerCase()
|
||||||
if (ranks.length === 0) {
|
|
||||||
return 50
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const median = getMedian(ranks)
|
|
||||||
|
|
||||||
return median
|
|
||||||
} catch {
|
} catch {
|
||||||
return 50
|
// Ignore decode errors and return an empty key for invalid values.
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestWithTimeout = async ({
|
||||||
|
relays,
|
||||||
|
filters,
|
||||||
|
timeout = RELAY_SEARCH_TIMEOUT_MS,
|
||||||
|
}: TimedRequestOptions): Promise<TrustedEvent[]> => {
|
||||||
|
if (relays.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController()
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await request({
|
||||||
|
relays,
|
||||||
|
autoClose: true,
|
||||||
|
threshold: REQUEST_THRESHOLD,
|
||||||
|
signal: controller.signal,
|
||||||
|
filters,
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getPubkeyRank = (pubkey: string): Promise<number | null> => {
|
const normalizeSearchTerm = (value: unknown): string => {
|
||||||
if (rankCache.has(pubkey)) {
|
if (typeof value !== "string") {
|
||||||
return Promise.resolve(rankCache.get(pubkey) ?? 50)
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
const pending = pendingRankRequests.get(pubkey)
|
return value.trim().replace(/\s+/g, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
const dedupeProfileTerms = (terms: string[]) => {
|
||||||
|
const seen = new Set<string>()
|
||||||
|
|
||||||
|
return terms.filter(term => {
|
||||||
|
if (term.length < MIN_PROFILE_TERM_LENGTH) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = term.toLowerCase()
|
||||||
|
|
||||||
|
if (seen.has(key)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
seen.add(key)
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTermsFromProfileRecord = (profile: Record<string, unknown>) => {
|
||||||
|
const terms = [
|
||||||
|
normalizeSearchTerm(profile.name),
|
||||||
|
normalizeSearchTerm(profile.display_name),
|
||||||
|
normalizeSearchTerm(profile.nip05),
|
||||||
|
]
|
||||||
|
|
||||||
|
const nip05 = normalizeSearchTerm(profile.nip05)
|
||||||
|
|
||||||
|
if (nip05.includes("@")) {
|
||||||
|
terms.push(normalizeSearchTerm(nip05.split("@")[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
return dedupeProfileTerms(terms).slice(0, MAX_PROFILE_TERMS)
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseSupportedNips = (nip11: Record<string, unknown>) =>
|
||||||
|
[...toStringArray(nip11.supported_nips), ...toStringArray(nip11.supportedNips)]
|
||||||
|
.map(nip => nip.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter((nip, index, nips) => nips.indexOf(nip) === index)
|
||||||
|
|
||||||
|
const parseNip50Extensions = (nip11: Record<string, unknown>) => {
|
||||||
|
const extensions = [
|
||||||
|
...toStringArray(nip11.nip50),
|
||||||
|
...toStringArray(nip11.nip_50),
|
||||||
|
...toStringArray(nip11["nip-50"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
if (nip11.extensions && typeof nip11.extensions === "object") {
|
||||||
|
const extensionObject = nip11.extensions as Record<string, unknown>
|
||||||
|
|
||||||
|
extensions.push(...toStringArray(extensionObject.nip50))
|
||||||
|
extensions.push(...toStringArray(extensionObject.nip_50))
|
||||||
|
extensions.push(...toStringArray(extensionObject["nip-50"]))
|
||||||
|
}
|
||||||
|
|
||||||
|
return extensions
|
||||||
|
.map(extension => extension.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter((extension, index, items) => items.indexOf(extension) === index)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchRelayInfo = async (relay: string): Promise<Nip11RelayInfo> => {
|
||||||
|
const controller = new AbortController()
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), NIP11_TIMEOUT_MS)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(toNip11Url(relay), {
|
||||||
|
headers: {Accept: "application/nostr+json"},
|
||||||
|
signal: controller.signal,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return {supportsNip50: false, nip50Extensions: []}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nip11 = (await response.json()) as Record<string, unknown>
|
||||||
|
const supportedNips = parseSupportedNips(nip11)
|
||||||
|
const nip50Extensions = parseNip50Extensions(nip11)
|
||||||
|
|
||||||
|
return {
|
||||||
|
supportsNip50: supportedNips.includes("50") || nip50Extensions.length > 0,
|
||||||
|
nip50Extensions,
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return {supportsNip50: false, nip50Extensions: []}
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRelayInfo = (relay: string) => {
|
||||||
|
const pending = relayInfoCache.get(relay)
|
||||||
|
|
||||||
if (pending) {
|
if (pending) {
|
||||||
return pending
|
return pending
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestPromise = fetchPubkeyRank(pubkey).then(rank => {
|
const requestPromise = fetchRelayInfo(relay)
|
||||||
rankCache.set(pubkey, rank ?? 50)
|
|
||||||
pendingRankRequests.delete(pubkey)
|
|
||||||
|
|
||||||
return rank ?? 50
|
relayInfoCache.set(relay, requestPromise)
|
||||||
})
|
|
||||||
|
return requestPromise
|
||||||
pendingRankRequests.set(pubkey, requestPromise)
|
}
|
||||||
|
|
||||||
|
const getNip50Relays = async (): Promise<RelaySearchInfo[]> => {
|
||||||
|
if (EXTENDED_SEARCH_RELAYS.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const relayInfo = await Promise.all(
|
||||||
|
EXTENDED_SEARCH_RELAYS.map(async relay => {
|
||||||
|
const info = await getRelayInfo(relay)
|
||||||
|
|
||||||
|
if (!info.supportsNip50) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return {relay, extensions: info.nip50Extensions}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const supportedRelays = relayInfo.filter(
|
||||||
|
(info): info is RelaySearchInfo => Boolean(info && info.extensions) && Boolean(info?.relay),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (supportedRelays.length > 0) {
|
||||||
|
return supportedRelays
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some relays block cross-origin NIP-11 fetches in browser contexts.
|
||||||
|
// If that happens, still run NIP-50 requests against configured relays.
|
||||||
|
return EXTENDED_SEARCH_RELAYS.map(relay => ({relay, extensions: []}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const dedupeByPubkey = (events: TrustedEvent[]) => {
|
||||||
|
const seen = new Set<string>()
|
||||||
|
const deduped: TrustedEvent[] = []
|
||||||
|
|
||||||
|
for (const event of events) {
|
||||||
|
if (seen.has(event.pubkey)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
seen.add(event.pubkey)
|
||||||
|
deduped.push(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
return deduped
|
||||||
|
}
|
||||||
|
|
||||||
|
const interleaveByPubkey = (eventLists: TrustedEvent[][]) => {
|
||||||
|
const offsets = new Array(eventLists.length).fill(0)
|
||||||
|
const seen = new Set<string>()
|
||||||
|
const interleaved: TrustedEvent[] = []
|
||||||
|
let added = true
|
||||||
|
|
||||||
|
while (added) {
|
||||||
|
added = false
|
||||||
|
|
||||||
|
for (let index = 0; index < eventLists.length; index += 1) {
|
||||||
|
const events = eventLists[index]
|
||||||
|
|
||||||
|
while (offsets[index] < events.length) {
|
||||||
|
const event = events[offsets[index]]
|
||||||
|
offsets[index] += 1
|
||||||
|
|
||||||
|
if (seen.has(event.pubkey)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
seen.add(event.pubkey)
|
||||||
|
interleaved.push(event)
|
||||||
|
added = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return interleaved
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSearchQuery = (pubkey: string, extensions: string[]) => {
|
||||||
|
const hasExactPhraseMatch = extensions.some(extension =>
|
||||||
|
extension.toLowerCase().includes("exact-phrase"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (hasExactPhraseMatch) {
|
||||||
|
return `"${pubkey}"`
|
||||||
|
}
|
||||||
|
|
||||||
|
return pubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
const getProfileSearchTerms = async (pubkey: string, relays: string[]) => {
|
||||||
|
const localProfile = getProfile(pubkey) as Record<string, unknown> | undefined
|
||||||
|
const localTerms = localProfile ? getTermsFromProfileRecord(localProfile) : []
|
||||||
|
|
||||||
|
if (localTerms.length >= MAX_PROFILE_TERMS || relays.length === 0) {
|
||||||
|
return localTerms.slice(0, MAX_PROFILE_TERMS)
|
||||||
|
}
|
||||||
|
|
||||||
|
const relayResults = await Promise.all(
|
||||||
|
relays.map(relay =>
|
||||||
|
requestWithTimeout({
|
||||||
|
relays: [relay],
|
||||||
|
timeout: PROFILE_LOOKUP_TIMEOUT_MS,
|
||||||
|
filters: [{kinds: [0], authors: [pubkey], limit: 1}],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const newestFirst = relayResults.flat().sort((a, b) => b.created_at - a.created_at)
|
||||||
|
|
||||||
|
for (const event of newestFirst) {
|
||||||
|
try {
|
||||||
|
const profile = JSON.parse(event.content || "{}") as Record<string, unknown>
|
||||||
|
const mergedTerms = dedupeProfileTerms([...localTerms, ...getTermsFromProfileRecord(profile)])
|
||||||
|
|
||||||
|
if (mergedTerms.length > 0) {
|
||||||
|
return mergedTerms.slice(0, MAX_PROFILE_TERMS)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return localTerms.slice(0, MAX_PROFILE_TERMS)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSearchTerms = (pubkey: string, extensions: string[], profileTerms: string[]) => {
|
||||||
|
const terms = [...profileTerms, getSearchQuery(pubkey, extensions)]
|
||||||
|
|
||||||
|
try {
|
||||||
|
terms.push(nip19.npubEncode(pubkey))
|
||||||
|
} catch {
|
||||||
|
// If pubkey is malformed, keep using the raw search term only.
|
||||||
|
}
|
||||||
|
|
||||||
|
return terms.filter((term, index, items) => Boolean(term) && items.indexOf(term) === index)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchRelayResults = async (
|
||||||
|
relay: string,
|
||||||
|
pubkey: string,
|
||||||
|
extensions: string[],
|
||||||
|
profileTerms: string[],
|
||||||
|
): Promise<TrustedEvent[]> => {
|
||||||
|
const terms = getSearchTerms(pubkey, extensions, profileTerms)
|
||||||
|
|
||||||
|
const results = await Promise.all(
|
||||||
|
terms.map(async term => {
|
||||||
|
const filters: Filter[] = [{kinds: [0], search: term, limit: SEARCH_LIMIT}]
|
||||||
|
|
||||||
|
return requestWithTimeout({
|
||||||
|
relays: [relay],
|
||||||
|
filters,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return dedupeByPubkey(results.flat())
|
||||||
|
}
|
||||||
|
|
||||||
|
const getScoreFromResults = (pubkey: string, relayResults: TrustedEvent[][]): ScoreResult => {
|
||||||
|
if (relayResults.length === 0) {
|
||||||
|
return {
|
||||||
|
score: DEFAULT_SCORE,
|
||||||
|
index: -1,
|
||||||
|
relayHits: 0,
|
||||||
|
activeRelays: 0,
|
||||||
|
interleavedCount: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeRelays = relayResults.filter(results => results.length > 0).length
|
||||||
|
const interleaved = interleaveByPubkey(relayResults)
|
||||||
|
|
||||||
|
if (activeRelays === 0 || interleaved.length === 0) {
|
||||||
|
return {
|
||||||
|
score: DEFAULT_SCORE,
|
||||||
|
index: -1,
|
||||||
|
relayHits: 0,
|
||||||
|
activeRelays,
|
||||||
|
interleavedCount: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = interleaved.findIndex(event => event.pubkey === pubkey)
|
||||||
|
const relayHits = relayResults.filter(results =>
|
||||||
|
results.some(event => event.pubkey === pubkey),
|
||||||
|
).length
|
||||||
|
const coverage = relayHits / Math.max(1, activeRelays)
|
||||||
|
const position = index === -1 ? 0 : 1 - index / Math.max(1, interleaved.length - 1)
|
||||||
|
const weightedScore = (coverage * 0.55 + position * 0.45) * 100
|
||||||
|
|
||||||
|
return {
|
||||||
|
score: clamp([0, 100], Math.round(weightedScore)),
|
||||||
|
index,
|
||||||
|
relayHits,
|
||||||
|
activeRelays,
|
||||||
|
interleavedCount: interleaved.length,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchPubkeyRank = async (inputPubkey: string): Promise<number> => {
|
||||||
|
const pubkey = normalizePubkey(inputPubkey)
|
||||||
|
|
||||||
|
if (!pubkey) {
|
||||||
|
return DEFAULT_SCORE
|
||||||
|
}
|
||||||
|
|
||||||
|
const relays = await getNip50Relays()
|
||||||
|
|
||||||
|
if (relays.length === 0) {
|
||||||
|
return DEFAULT_SCORE
|
||||||
|
}
|
||||||
|
|
||||||
|
const profileTerms = await getProfileSearchTerms(
|
||||||
|
pubkey,
|
||||||
|
relays.map(({relay}) => relay),
|
||||||
|
)
|
||||||
|
|
||||||
|
const relayResults = await Promise.all(
|
||||||
|
relays.map(({relay, extensions}) => fetchRelayResults(relay, pubkey, extensions, profileTerms)),
|
||||||
|
)
|
||||||
|
|
||||||
|
const scoreResult = getScoreFromResults(pubkey, relayResults)
|
||||||
|
|
||||||
|
console.log(`[Score Calc] Pubkey: ${pubkey} → Score: ${scoreResult.score}`)
|
||||||
|
|
||||||
|
return scoreResult.score
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPubkeyRank = (pubkey: string): Promise<number | null> => {
|
||||||
|
const normalizedPubkey = normalizePubkey(pubkey)
|
||||||
|
|
||||||
|
if (!normalizedPubkey) {
|
||||||
|
return Promise.resolve(DEFAULT_SCORE)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cached = rankCache.get(normalizedPubkey)
|
||||||
|
|
||||||
|
if (cached !== undefined) {
|
||||||
|
return Promise.resolve(cached)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pending = pendingRankRequests.get(normalizedPubkey)
|
||||||
|
|
||||||
|
if (pending) {
|
||||||
|
return pending
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestPromise = fetchPubkeyRank(normalizedPubkey).then(rank => {
|
||||||
|
rankCache.set(normalizedPubkey, rank)
|
||||||
|
pendingRankRequests.delete(normalizedPubkey)
|
||||||
|
|
||||||
|
return rank
|
||||||
|
})
|
||||||
|
|
||||||
|
pendingRankRequests.set(normalizedPubkey, requestPromise)
|
||||||
|
|
||||||
return requestPromise
|
return requestPromise
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user