|
|
|
@ -1,13 +1,20 @@ |
|
|
|
import { TRelayGroup } from '@common/types' |
|
|
|
import { TDraftEvent, TRelayGroup } from '@common/types' |
|
|
|
import { formatPubkey } from '@renderer/lib/pubkey' |
|
|
|
import { formatPubkey } from '@renderer/lib/pubkey' |
|
|
|
import { tagNameEquals } from '@renderer/lib/tag' |
|
|
|
import { tagNameEquals } from '@renderer/lib/tag' |
|
|
|
|
|
|
|
import { isWebsocketUrl, normalizeUrl } from '@renderer/lib/url' |
|
|
|
import { TProfile, TRelayList } from '@renderer/types' |
|
|
|
import { TProfile, TRelayList } from '@renderer/types' |
|
|
|
import DataLoader from 'dataloader' |
|
|
|
import DataLoader from 'dataloader' |
|
|
|
import { LRUCache } from 'lru-cache' |
|
|
|
import { LRUCache } from 'lru-cache' |
|
|
|
import { Filter, kinds, Event as NEvent, SimplePool } from 'nostr-tools' |
|
|
|
import { |
|
|
|
|
|
|
|
EventTemplate, |
|
|
|
|
|
|
|
Filter, |
|
|
|
|
|
|
|
kinds, |
|
|
|
|
|
|
|
Event as NEvent, |
|
|
|
|
|
|
|
SimplePool, |
|
|
|
|
|
|
|
VerifiedEvent |
|
|
|
|
|
|
|
} from 'nostr-tools' |
|
|
|
import { EVENT_TYPES, eventBus } from './event-bus.service' |
|
|
|
import { EVENT_TYPES, eventBus } from './event-bus.service' |
|
|
|
import storage from './storage.service' |
|
|
|
import storage from './storage.service' |
|
|
|
import { isWebsocketUrl, normalizeUrl } from '@renderer/lib/url' |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const BIG_RELAY_URLS = [ |
|
|
|
const BIG_RELAY_URLS = [ |
|
|
|
'wss://relay.damus.io/', |
|
|
|
'wss://relay.damus.io/', |
|
|
|
@ -26,10 +33,7 @@ class ClientService { |
|
|
|
private eventByFilterCache = new LRUCache<string, Promise<NEvent | undefined>>({ |
|
|
|
private eventByFilterCache = new LRUCache<string, Promise<NEvent | undefined>>({ |
|
|
|
max: 10000, |
|
|
|
max: 10000, |
|
|
|
fetchMethod: async (filterStr) => { |
|
|
|
fetchMethod: async (filterStr) => { |
|
|
|
const events = await this.fetchEvents( |
|
|
|
const events = await this.fetchEvents(BIG_RELAY_URLS, JSON.parse(filterStr)) |
|
|
|
BIG_RELAY_URLS.concat(this.relayUrls), |
|
|
|
|
|
|
|
JSON.parse(filterStr) |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
events.forEach((event) => this.addEventToCache(event)) |
|
|
|
events.forEach((event) => this.addEventToCache(event)) |
|
|
|
return events.sort((a, b) => b.created_at - a.created_at)[0] |
|
|
|
return events.sort((a, b) => b.created_at - a.created_at)[0] |
|
|
|
} |
|
|
|
} |
|
|
|
@ -85,44 +89,86 @@ class ClientService { |
|
|
|
return await Promise.any(this.pool.publish(this.relayUrls.concat(relayUrls), event)) |
|
|
|
return await Promise.any(this.pool.publish(this.relayUrls.concat(relayUrls), event)) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
subscribeEvents( |
|
|
|
subscribeEventsWithAuth( |
|
|
|
urls: string[], |
|
|
|
urls: string[], |
|
|
|
filter: Filter, |
|
|
|
filter: Filter, |
|
|
|
opts: { |
|
|
|
{ |
|
|
|
|
|
|
|
onEose, |
|
|
|
|
|
|
|
onNew |
|
|
|
|
|
|
|
}: { |
|
|
|
onEose: (events: NEvent[]) => void |
|
|
|
onEose: (events: NEvent[]) => void |
|
|
|
onNew: (evt: NEvent) => void |
|
|
|
onNew: (evt: NEvent) => void |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
signer?: (evt: TDraftEvent) => Promise<NEvent> |
|
|
|
) { |
|
|
|
) { |
|
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
|
|
|
|
|
|
const that = this |
|
|
|
|
|
|
|
const _knownIds = new Set<string>() |
|
|
|
const events: NEvent[] = [] |
|
|
|
const events: NEvent[] = [] |
|
|
|
let eose = false |
|
|
|
let started = 0 |
|
|
|
return this.pool.subscribeMany( |
|
|
|
let eosed = 0 |
|
|
|
urls.length > 0 ? urls : this.relayUrls.concat(BIG_RELAY_URLS), |
|
|
|
const subPromises = urls.map(async (url) => { |
|
|
|
[filter], |
|
|
|
const relay = await this.pool.ensureRelay(url) |
|
|
|
{ |
|
|
|
let hasAuthed = false |
|
|
|
onevent: (evt) => { |
|
|
|
|
|
|
|
if (eose) { |
|
|
|
return startSub() |
|
|
|
opts.onNew(evt) |
|
|
|
|
|
|
|
|
|
|
|
function startSub() { |
|
|
|
|
|
|
|
started++ |
|
|
|
|
|
|
|
return relay.subscribe([filter], { |
|
|
|
|
|
|
|
alreadyHaveEvent: (id: string) => { |
|
|
|
|
|
|
|
const have = _knownIds.has(id) |
|
|
|
|
|
|
|
_knownIds.add(id) |
|
|
|
|
|
|
|
return have |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
onevent(evt: NEvent) { |
|
|
|
|
|
|
|
if (eosed === started) { |
|
|
|
|
|
|
|
onNew(evt) |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
events.push(evt) |
|
|
|
events.push(evt) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
that.eventByIdCache.set(evt.id, Promise.resolve(evt)) |
|
|
|
}, |
|
|
|
}, |
|
|
|
oneose: () => { |
|
|
|
onclose(reason: string) { |
|
|
|
eose = true |
|
|
|
if (reason.startsWith('auth-required:')) { |
|
|
|
opts.onEose(events.sort((a, b) => b.created_at - a.created_at)) |
|
|
|
if (!hasAuthed && signer) { |
|
|
|
|
|
|
|
relay |
|
|
|
|
|
|
|
.auth((authEvt: EventTemplate) => { |
|
|
|
|
|
|
|
return signer(authEvt) as Promise<VerifiedEvent> |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
.then(() => { |
|
|
|
|
|
|
|
hasAuthed = true |
|
|
|
|
|
|
|
startSub() |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
}, |
|
|
|
}, |
|
|
|
onclose: () => { |
|
|
|
oneose() { |
|
|
|
if (!eose) { |
|
|
|
eosed++ |
|
|
|
opts.onEose(events.sort((a, b) => b.created_at - a.created_at)) |
|
|
|
if (eosed === started) { |
|
|
|
|
|
|
|
events.sort((a, b) => b.created_at - a.created_at) |
|
|
|
|
|
|
|
onEose(events) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return () => { |
|
|
|
|
|
|
|
onEose = () => {} |
|
|
|
|
|
|
|
onNew = () => {} |
|
|
|
|
|
|
|
subPromises.forEach((subPromise) => { |
|
|
|
|
|
|
|
subPromise.then((sub) => { |
|
|
|
|
|
|
|
sub.close() |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async fetchEvents(relayUrls: string[], filter: Filter) { |
|
|
|
async fetchEvents(relayUrls: string[], filter: Filter) { |
|
|
|
await this.initPromise |
|
|
|
await this.initPromise |
|
|
|
// If relayUrls is empty, use this.relayUrls
|
|
|
|
// If relayUrls is empty, use this.relayUrls
|
|
|
|
return await this.pool.querySync(relayUrls.length > 0 ? relayUrls : this.relayUrls, filter) |
|
|
|
return await this.pool.querySync(relayUrls.length > 0 ? relayUrls : BIG_RELAY_URLS, filter) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async fetchEventByFilter(filter: Filter) { |
|
|
|
async fetchEventByFilter(filter: Filter) { |
|
|
|
@ -154,7 +200,7 @@ class ClientService { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private async eventBatchLoadFn(ids: readonly string[]) { |
|
|
|
private async eventBatchLoadFn(ids: readonly string[]) { |
|
|
|
const events = await this.fetchEvents(this.relayUrls, { |
|
|
|
const events = await this.fetchEvents(BIG_RELAY_URLS, { |
|
|
|
ids: ids as string[], |
|
|
|
ids: ids as string[], |
|
|
|
limit: ids.length |
|
|
|
limit: ids.length |
|
|
|
}) |
|
|
|
}) |
|
|
|
@ -163,25 +209,11 @@ class ClientService { |
|
|
|
eventsMap.set(event.id, event) |
|
|
|
eventsMap.set(event.id, event) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const missingIds = ids.filter((id) => !eventsMap.has(id)) |
|
|
|
|
|
|
|
if (missingIds.length > 0) { |
|
|
|
|
|
|
|
const missingEvents = await this.fetchEvents( |
|
|
|
|
|
|
|
BIG_RELAY_URLS.filter((url) => !this.relayUrls.includes(url)), |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
ids: missingIds, |
|
|
|
|
|
|
|
limit: missingIds.length |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
for (const event of missingEvents) { |
|
|
|
|
|
|
|
eventsMap.set(event.id, event) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return ids.map((id) => eventsMap.get(id)) |
|
|
|
return ids.map((id) => eventsMap.get(id)) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private async profileBatchLoadFn(pubkeys: readonly string[]) { |
|
|
|
private async profileBatchLoadFn(pubkeys: readonly string[]) { |
|
|
|
const events = await this.fetchEvents(this.relayUrls, { |
|
|
|
const events = await this.fetchEvents(BIG_RELAY_URLS, { |
|
|
|
authors: pubkeys as string[], |
|
|
|
authors: pubkeys as string[], |
|
|
|
kinds: [kinds.Metadata], |
|
|
|
kinds: [kinds.Metadata], |
|
|
|
limit: pubkeys.length |
|
|
|
limit: pubkeys.length |
|
|
|
@ -195,25 +227,6 @@ class ClientService { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const missingPubkeys = pubkeys.filter((pubkey) => !eventsMap.has(pubkey)) |
|
|
|
|
|
|
|
if (missingPubkeys.length > 0) { |
|
|
|
|
|
|
|
const missingEvents = await this.fetchEvents( |
|
|
|
|
|
|
|
BIG_RELAY_URLS.filter((url) => !this.relayUrls.includes(url)), |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
authors: missingPubkeys, |
|
|
|
|
|
|
|
kinds: [kinds.Metadata], |
|
|
|
|
|
|
|
limit: missingPubkeys.length |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
for (const event of missingEvents) { |
|
|
|
|
|
|
|
const pubkey = event.pubkey |
|
|
|
|
|
|
|
const existing = eventsMap.get(pubkey) |
|
|
|
|
|
|
|
if (!existing || existing.created_at < event.created_at) { |
|
|
|
|
|
|
|
eventsMap.set(pubkey, event) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return pubkeys.map((pubkey) => { |
|
|
|
return pubkeys.map((pubkey) => { |
|
|
|
const event = eventsMap.get(pubkey) |
|
|
|
const event = eventsMap.get(pubkey) |
|
|
|
return event ? this.parseProfileFromEvent(event) : undefined |
|
|
|
return event ? this.parseProfileFromEvent(event) : undefined |
|
|
|
@ -221,7 +234,7 @@ class ClientService { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private async relayListBatchLoadFn(pubkeys: readonly string[]) { |
|
|
|
private async relayListBatchLoadFn(pubkeys: readonly string[]) { |
|
|
|
const events = await this.fetchEvents(BIG_RELAY_URLS.concat(this.relayUrls), { |
|
|
|
const events = await this.fetchEvents(BIG_RELAY_URLS, { |
|
|
|
authors: pubkeys as string[], |
|
|
|
authors: pubkeys as string[], |
|
|
|
kinds: [kinds.RelayList], |
|
|
|
kinds: [kinds.RelayList], |
|
|
|
limit: pubkeys.length |
|
|
|
limit: pubkeys.length |
|
|
|
|