Fix ts errors

This commit is contained in:
Jon Staab
2025-03-31 12:49:28 -07:00
parent cfd2e3aac7
commit 1524d128e3
27 changed files with 357 additions and 448 deletions
+1
View File
@@ -30,6 +30,7 @@
"@welshman/dvm": "^0.0.15",
"@welshman/feeds": "^0.1.0",
"@welshman/lib": "^0.1.0",
"@welshman/relay": "^0.1.0",
"@welshman/net": "^0.0.49",
"@welshman/signer": "^0.1.0",
"@welshman/store": "^0.1.0",
+7 -7
View File
@@ -1,48 +1,48 @@
import {get} from "svelte/store"
import {ctx} from "@welshman/lib"
import {addToListPublicly, removeFromList, makeList, FOLLOWS, MUTES, PINS} from "@welshman/util"
import {userFollows, userMutes, userPins} from "./user.js"
import {nip44EncryptToSelf} from "./session.js"
import {publishThunk} from "./thunk.js"
import {Router} from "./router.js"
export const unfollow = async (value: string) => {
const list = get(userFollows) || makeList({kind: FOLLOWS})
const event = await removeFromList(list, value).reconcile(nip44EncryptToSelf)
return publishThunk({event, relays: ctx.app.router.FromUser().getUrls()})
return publishThunk({event, relays: Router.getInstance().FromUser().getUrls()})
}
export const follow = async (tag: string[]) => {
const list = get(userFollows) || makeList({kind: FOLLOWS})
const event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf)
return publishThunk({event, relays: ctx.app.router.FromUser().getUrls()})
return publishThunk({event, relays: Router.getInstance().FromUser().getUrls()})
}
export const unmute = async (value: string) => {
const list = get(userMutes) || makeList({kind: MUTES})
const event = await removeFromList(list, value).reconcile(nip44EncryptToSelf)
return publishThunk({event, relays: ctx.app.router.FromUser().getUrls()})
return publishThunk({event, relays: Router.getInstance().FromUser().getUrls()})
}
export const mute = async (tag: string[]) => {
const list = get(userMutes) || makeList({kind: MUTES})
const event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf)
return publishThunk({event, relays: ctx.app.router.FromUser().getUrls()})
return publishThunk({event, relays: Router.getInstance().FromUser().getUrls()})
}
export const unpin = async (value: string) => {
const list = get(userPins) || makeList({kind: PINS})
const event = await removeFromList(list, value).reconcile(nip44EncryptToSelf)
return publishThunk({event, relays: ctx.app.router.FromUser().getUrls()})
return publishThunk({event, relays: Router.getInstance().FromUser().getUrls()})
}
export const pin = async (tag: string[]) => {
const list = get(userPins) || makeList({kind: PINS})
const event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf)
return publishThunk({event, relays: ctx.app.router.FromUser().getUrls()})
return publishThunk({event, relays: Router.getInstance().FromUser().getUrls()})
}
+3 -54
View File
@@ -1,57 +1,6 @@
import {partition} from "@welshman/lib"
import {
defaultOptimizeSubscriptions,
getDefaultNetContext as originalGetDefaultNetContext,
} from "@welshman/net"
import type {Subscription, RelaysAndFilters, NetContext} from "@welshman/net"
import {LOCAL_RELAY_URL, isEphemeralKind, isDVMKind, unionFilters} from "@welshman/util"
import type {TrustedEvent, StampedEvent} from "@welshman/util"
import {tracker, repository} from "./core.js"
import {makeRouter, getFilterSelections} from "./router.js"
import {signer} from "./session.js"
import type {Router} from "./router.js"
export type AppContext = {
router: Router
requestDelay: number
authTimeout: number
requestTimeout: number
export const AppContext: {
dufflepudUrl?: string
indexerRelays?: string[]
} = {
indexerRelays: ["wss://purplepag.es/", "wss://relay.nostr.band/", "wss://relay.primal.net/"],
}
export const getDefaultNetContext = (overrides: Partial<NetContext> = {}) => ({
...originalGetDefaultNetContext(),
signEvent: async (event: StampedEvent) => signer.get()?.sign(event),
onEvent: (url: string, event: TrustedEvent) => {
if (isEphemeralKind(event.kind) || isDVMKind(event.kind)) return
tracker.track(event.id, url)
repository.publish(event)
},
isDeleted: (url: string, event: TrustedEvent) => repository.isDeleted(event),
optimizeSubscriptions: (subs: Subscription[]) => {
const [withRelays, withoutRelays] = partition(sub => sub.request.relays.length > 0, subs)
const filters = unionFilters(withoutRelays.flatMap(sub => sub.request.filters))
const selections: RelaysAndFilters[] = defaultOptimizeSubscriptions(withRelays)
selections.push({relays: [LOCAL_RELAY_URL], filters})
if (filters.length > 0) {
for (const selection of getFilterSelections(filters)) {
selections.push(selection)
}
}
return selections
},
...overrides,
})
export const getDefaultAppContext = (overrides: Partial<AppContext> = {}) => ({
router: makeRouter(),
requestDelay: 50,
authTimeout: 300,
requestTimeout: 3000,
...overrides,
})
+2 -2
View File
@@ -1,11 +1,11 @@
import {throttle} from "@welshman/lib"
import {Repository, Relay} from "@welshman/util"
import {Repository, LocalRelay} from "@welshman/relay"
import {Tracker} from "@welshman/net"
import {custom} from "@welshman/store"
export const repository = Repository.getSingleton()
export const relay = new Relay(repository)
export const relay = new LocalRelay(repository)
export const tracker = new Tracker()
+18 -9
View File
@@ -1,20 +1,29 @@
import {ctx, nthEq, now} from "@welshman/lib"
import {nthEq, now} from "@welshman/lib"
import {createEvent, getPubkeyTagValues} from "@welshman/util"
import {Scope, FeedController} from "@welshman/feeds"
import type {RequestOpts, FeedOptions, DVMOpts, Feed} from "@welshman/feeds"
import {MultiRequest, RequestEvent} from "@welshman/net"
import {Scope, FeedController, RequestOpts, FeedOptions, DVMOpts, Feed} from "@welshman/feeds"
import {makeDvmRequest, DVMEvent} from "@welshman/dvm"
import {makeSecret, Nip01Signer} from "@welshman/signer"
import {pubkey, signer} from "./session.js"
import {getFilterSelections} from "./router.js"
import {Router, getFilterSelections} from "./router.js"
import {loadRelaySelections} from "./relaySelections.js"
import {wotGraph, maxWot, getFollows, getNetwork, getFollowers} from "./wot.js"
import {load} from "./subscribe.js"
export const request = async ({filters = [{}], relays = [], onEvent}: RequestOpts) => {
if (relays.length > 0) {
await load({onEvent, filters, relays})
await Promise.all(
filters.map(
filter =>
new Promise<void>(resolve => {
const sub = new MultiRequest({filter, relays, timeout: 5000, autoClose: true})
sub.on(RequestEvent.Event, onEvent)
sub.on(RequestEvent.Close, resolve)
}),
),
)
} else {
await Promise.all(getFilterSelections(filters).map(opts => load({onEvent, ...opts})))
await Promise.all(getFilterSelections(filters).map(opts => request({...opts, onEvent})))
}
}
@@ -30,8 +39,8 @@ export const requestDVM = async ({kind, onEvent, ...request}: DVMOpts) => {
const $signer = signer.get() || new Nip01Signer(makeSecret())
const pubkey = await $signer.getPubkey()
const relays = request.relays
? ctx.app.router.FromRelays(request.relays).getUrls()
: ctx.app.router.FromPubkeys(getPubkeyTagValues(tags)).getUrls()
? Router.getInstance().FromRelays(request.relays).getUrls()
: Router.getInstance().FromPubkeys(getPubkeyTagValues(tags)).getUrls()
if (!tags.some(nthEq(0, "expiration"))) {
tags.push(["expiration", String(now() + 60)])
+9 -5
View File
@@ -1,9 +1,9 @@
import {FOLLOWS, asDecryptedEvent, readList} from "@welshman/util"
import {type TrustedEvent, type PublishedList} from "@welshman/util"
import {type SubscribeRequestWithHandlers} from "@welshman/net"
import {TrustedEvent, PublishedList} from "@welshman/util"
import {MultiRequestOptions, load} from "@welshman/net"
import {deriveEventsMapped} from "@welshman/store"
import {repository} from "./core.js"
import {load} from "./subscribe.js"
import {Router} from "./router.js"
import {collection} from "./collection.js"
import {loadRelaySelections} from "./relaySelections.js"
@@ -21,8 +21,12 @@ export const {
name: "follows",
store: follows,
getKey: follows => follows.event.pubkey,
load: async (pubkey: string, request: Partial<SubscribeRequestWithHandlers> = {}) => {
load: async (pubkey: string, request: Partial<MultiRequestOptions> = {}) => {
await loadRelaySelections(pubkey, request)
await load({...request, filters: [{kinds: [FOLLOWS], authors: [pubkey]}]})
const filter = {kinds: [FOLLOWS], authors: [pubkey]}
const relays = Router.getInstance().FromPubkey(pubkey).getUrls()
await load({relays, ...request, filter})
},
})
+5 -7
View File
@@ -1,8 +1,9 @@
import {writable, derived} from "svelte/store"
import {type SubscribeRequestWithHandlers} from "@welshman/net"
import {ctx, tryCatch, fetchJson, uniq, batcher, postJson, last} from "@welshman/lib"
import {MultiRequestOptions} from "@welshman/net"
import {tryCatch, fetchJson, uniq, batcher, postJson, last} from "@welshman/lib"
import {collection} from "./collection.js"
import {deriveProfile} from "./profiles.js"
import {AppContext} from "./context.js"
export type Handle = {
nip05: string
@@ -47,7 +48,7 @@ export async function queryProfile(nip05: string) {
export const handles = writable<Handle[]>([])
export const fetchHandles = async (nip05s: string[]) => {
const base = ctx.app.dufflepudUrl!
const base = AppContext.dufflepudUrl!
const handlesByNip05 = new Map<string, Handle>()
// Use dufflepud if we it's set up to protect user privacy, otherwise fetch directly
@@ -103,10 +104,7 @@ export const {
}),
})
export const deriveHandleForPubkey = (
pubkey: string,
request: Partial<SubscribeRequestWithHandlers> = {},
) =>
export const deriveHandleForPubkey = (pubkey: string, request: Partial<MultiRequestOptions> = {}) =>
derived([handlesByNip05, deriveProfile(pubkey, request)], ([$handlesByNip05, $profile]) => {
if (!$profile?.nip05) {
return undefined
-11
View File
@@ -16,7 +16,6 @@ export * from "./router.js"
export * from "./search.js"
export * from "./session.js"
export * from "./storage.js"
export * from "./subscribe.js"
export * from "./sync.js"
export * from "./tags.js"
export * from "./thunk.js"
@@ -25,13 +24,3 @@ export * from "./user.js"
export * from "./util.js"
export * from "./wot.js"
export * from "./zappers.js"
import type {NetContext} from "@welshman/net"
import type {AppContext} from "./context.js"
declare module "@welshman/lib" {
interface Context {
net: NetContext
app: AppContext
}
}
+9 -5
View File
@@ -1,9 +1,9 @@
import {MUTES, asDecryptedEvent, readList} from "@welshman/util"
import {type TrustedEvent, type PublishedList} from "@welshman/util"
import {type SubscribeRequestWithHandlers} from "@welshman/net"
import {TrustedEvent, PublishedList} from "@welshman/util"
import {load, MultiRequestOptions} from "@welshman/net"
import {deriveEventsMapped} from "@welshman/store"
import {repository} from "./core.js"
import {load} from "./subscribe.js"
import {Router} from "./router.js"
import {collection} from "./collection.js"
import {ensurePlaintext} from "./plaintext.js"
import {loadRelaySelections} from "./relaySelections.js"
@@ -27,8 +27,12 @@ export const {
name: "mutes",
store: mutes,
getKey: mute => mute.event.pubkey,
load: async (pubkey: string, request: Partial<SubscribeRequestWithHandlers> = {}) => {
load: async (pubkey: string, request: Partial<MultiRequestOptions> = {}) => {
await loadRelaySelections(pubkey, request)
await load({...request, filters: [{kinds: [MUTES], authors: [pubkey]}]})
const filter = {kinds: [MUTES], authors: [pubkey]}
const relays = Router.getInstance().FromPubkey(pubkey).getUrls()
await load({relays, ...request, filter})
},
})
+9 -5
View File
@@ -1,9 +1,9 @@
import {PINS, asDecryptedEvent, readList} from "@welshman/util"
import {type TrustedEvent, type PublishedList} from "@welshman/util"
import {type SubscribeRequestWithHandlers} from "@welshman/net"
import {TrustedEvent, PublishedList} from "@welshman/util"
import {load, MultiRequestOptions} from "@welshman/net"
import {deriveEventsMapped} from "@welshman/store"
import {repository} from "./core.js"
import {load} from "./subscribe.js"
import {Router} from "./router.js"
import {collection} from "./collection.js"
import {loadRelaySelections} from "./relaySelections.js"
@@ -21,8 +21,12 @@ export const {
name: "pins",
store: pins,
getKey: pins => pins.event.pubkey,
load: async (pubkey: string, request: Partial<SubscribeRequestWithHandlers> = {}) => {
load: async (pubkey: string, request: Partial<MultiRequestOptions> = {}) => {
await loadRelaySelections(pubkey, request)
await load({...request, filters: [{kinds: [PINS], authors: [pubkey]}]})
const filter = {kinds: [PINS], authors: [pubkey]}
const relays = Router.getInstance().FromPubkey(pubkey).getUrls()
await load({relays, ...request, filter})
},
})
+9 -18
View File
@@ -1,10 +1,10 @@
import {derived, readable} from "svelte/store"
import {readProfile, displayProfile, displayPubkey, PROFILE} from "@welshman/util"
import type {SubscribeRequestWithHandlers} from "@welshman/net"
import type {PublishedProfile} from "@welshman/util"
import {load, MultiRequestOptions} from "@welshman/net"
import {PublishedProfile} from "@welshman/util"
import {deriveEventsMapped, withGetter} from "@welshman/store"
import {repository} from "./core.js"
import {load} from "./subscribe.js"
import {Router} from "./router.js"
import {collection} from "./collection.js"
import {loadRelaySelections} from "./relaySelections.js"
@@ -24,23 +24,14 @@ export const {
name: "profiles",
store: profiles,
getKey: profile => profile.event.pubkey,
load: async (pubkey: string, request: Partial<SubscribeRequestWithHandlers> = {}) => {
const filters = [{kinds: [PROFILE], authors: [pubkey]}]
load: async (pubkey: string, request: Partial<MultiRequestOptions> = {}) => {
await loadRelaySelections(pubkey, request)
// Attempt to load the user profile right away, regardless of whether we have relays,
// since profiles are crucial to UX
load({...request, filters})
const router = Router.getInstance()
const filter = {kinds: [PROFILE], authors: [pubkey]}
const relays = router.merge([router.Index(), router.FromPubkey(pubkey)]).getUrls()
// Load relay selections as quickly as possible, moving on to retrying profiles with
// better selections the moment we have a result, even if it's outdated
await new Promise<void>(resolve => {
loadRelaySelections(pubkey, {
onEvent: () => resolve(),
onComplete: () => resolve(),
})
})
await load({...request, filters})
await load({relays, ...request, filter})
},
})
+21 -7
View File
@@ -9,11 +9,11 @@ import {
getRelayTags,
getRelayTagValues,
} from "@welshman/util"
import type {TrustedEvent, PublishedList, List} from "@welshman/util"
import type {SubscribeRequestWithHandlers} from "@welshman/net"
import {TrustedEvent, PublishedList, List} from "@welshman/util"
import {load, MultiRequestOptions} from "@welshman/net"
import {deriveEventsMapped} from "@welshman/store"
import {repository} from "./core.js"
import {load} from "./subscribe.js"
import {Router} from "./router.js"
import {collection} from "./collection.js"
export const getRelayUrls = (list?: List): string[] =>
@@ -47,8 +47,15 @@ export const {
name: "relaySelections",
store: relaySelections,
getKey: relaySelections => relaySelections.event.pubkey,
load: (pubkey: string, request: Partial<SubscribeRequestWithHandlers> = {}) =>
load({...request, filters: [{kinds: [RELAYS], authors: [pubkey]}]}),
load: async (pubkey: string, request: Partial<MultiRequestOptions> = {}) => {
const router = Router.getInstance()
await load({
relays: router.merge([router.Index(), router.FromPubkey(pubkey)]).getUrls(),
...request,
filter: {kinds: [RELAYS], authors: [pubkey]},
})
},
})
export const inboxRelaySelections = deriveEventsMapped<PublishedList>(repository, {
@@ -65,6 +72,13 @@ export const {
name: "inboxRelaySelections",
store: inboxRelaySelections,
getKey: inboxRelaySelections => inboxRelaySelections.event.pubkey,
load: (pubkey: string, request: Partial<SubscribeRequestWithHandlers> = {}) =>
load({...request, filters: [{kinds: [INBOX_RELAYS], authors: [pubkey]}]}),
load: async (pubkey: string, request: Partial<MultiRequestOptions> = {}) => {
const router = Router.getInstance()
await load({
relays: router.merge([router.Index(), router.FromPubkey(pubkey)]).getUrls(),
...request,
filter: {kinds: [INBOX_RELAYS], authors: [pubkey]},
})
},
})
+46 -59
View File
@@ -1,11 +1,11 @@
import {writable, derived} from "svelte/store"
import {withGetter} from "@welshman/store"
import {ctx, groupBy, indexBy, batch, now, ago, uniq, batcher, postJson} from "@welshman/lib"
import type {RelayProfile} from "@welshman/util"
import {groupBy, indexBy, batch, now, uniq, batcher, postJson} from "@welshman/lib"
import {RelayProfile} from "@welshman/util"
import {normalizeRelayUrl, displayRelayUrl, displayRelayProfile} from "@welshman/util"
import {ConnectionEvent} from "@welshman/net"
import type {Connection, Message} from "@welshman/net"
import {Socket, SocketStatus, SocketEvent, ClientMessage, RelayMessage} from "@welshman/net"
import {collection} from "./collection.js"
import {AppContext} from "./context.js"
export type RelayStats = {
first_seen: number
@@ -22,11 +22,9 @@ export type RelayStats = {
last_request: number
last_event: number
last_auth: number
publish_timer: number
publish_success_count: number
publish_failure_count: number
eose_count: number
eose_timer: number
notice_count: number
}
@@ -45,11 +43,9 @@ export const makeRelayStats = (): RelayStats => ({
last_request: 0,
last_event: 0,
last_auth: 0,
publish_timer: 0,
publish_success_count: 0,
publish_failure_count: 0,
eose_count: 0,
eose_timer: 0,
notice_count: 0,
})
@@ -69,7 +65,7 @@ export const relaysByPubkey = derived(relays, $relays =>
)
export const fetchRelayProfiles = async (urls: string[]) => {
const base = ctx.app.dufflepudUrl!
const base = AppContext.dufflepudUrl
if (!base) {
throw new Error("ctx.app.dufflepudUrl is required to fetch relay metadata")
@@ -150,25 +146,7 @@ const updateRelayStats = batch(500, (updates: RelayStatsUpdate[]) => {
})
})
const onConnectionOpen = ({url}: Connection) =>
updateRelayStats([
url,
stats => {
stats.last_open = now()
stats.open_count++
},
])
const onConnectionClose = ({url}: Connection) =>
updateRelayStats([
url,
stats => {
stats.last_close = now()
stats.close_count++
},
])
const onConnectionSend = ({url}: Connection, [verb]: Message) => {
const onSocketSend = ([verb]: ClientMessage, url: string) => {
if (verb === "REQ") {
updateRelayStats([
url,
@@ -188,18 +166,13 @@ const onConnectionSend = ({url}: Connection, [verb]: Message) => {
}
}
const onConnectionReceive = ({url, state}: Connection, [verb, ...extra]: Message) => {
const onSocketReceive = ([verb, ...extra]: RelayMessage, url: string) => {
if (verb === "OK") {
const [eventId, ok] = extra
const pub = state.pendingPublishes.get(eventId)
const [_, ok] = extra
updateRelayStats([
url,
stats => {
if (pub) {
stats.publish_timer += ago(pub.sent)
}
if (ok) {
stats.publish_success_count++
} else {
@@ -223,18 +196,12 @@ const onConnectionReceive = ({url, state}: Connection, [verb, ...extra]: Message
},
])
} else if (verb === "EOSE") {
const request = state.pendingRequests.get(extra[0])
// Only count the first eose
if (request && !request.eose) {
updateRelayStats([
url,
stats => {
stats.eose_count++
stats.eose_timer += now() - request.sent
},
])
}
updateRelayStats([
url,
stats => {
stats.eose_count++
},
])
} else if (verb === "NOTICE") {
updateRelayStats([
url,
@@ -245,7 +212,29 @@ const onConnectionReceive = ({url, state}: Connection, [verb, ...extra]: Message
}
}
const onConnectionError = ({url}: Connection) =>
const onSocketStatus = (status: string, url: string) => {
if (status === SocketStatus.Open) {
updateRelayStats([
url,
stats => {
stats.last_open = now()
stats.open_count++
},
])
}
if (status === SocketStatus.Closed) {
updateRelayStats([
url,
stats => {
stats.last_close = now()
stats.close_count++
},
])
}
}
const onSocketError = (error: string, url: string) =>
updateRelayStats([
url,
stats => {
@@ -254,18 +243,16 @@ const onConnectionError = ({url}: Connection) =>
},
])
export const trackRelayStats = (connection: Connection) => {
connection.on(ConnectionEvent.Open, onConnectionOpen)
connection.on(ConnectionEvent.Close, onConnectionClose)
connection.on(ConnectionEvent.Send, onConnectionSend)
connection.on(ConnectionEvent.Receive, onConnectionReceive)
connection.on(ConnectionEvent.Error, onConnectionError)
export const trackRelayStats = (socket: Socket) => {
socket.on(SocketEvent.Send, onSocketSend)
socket.on(SocketEvent.Receive, onSocketReceive)
socket.on(SocketEvent.Status, onSocketStatus)
socket.on(SocketEvent.Error, onSocketError)
return () => {
connection.off(ConnectionEvent.Open, onConnectionOpen)
connection.off(ConnectionEvent.Close, onConnectionClose)
connection.off(ConnectionEvent.Send, onConnectionSend)
connection.off(ConnectionEvent.Receive, onConnectionReceive)
connection.off(ConnectionEvent.Error, onConnectionError)
socket.off(SocketEvent.Send, onSocketSend)
socket.off(SocketEvent.Receive, onSocketReceive)
socket.off(SocketEvent.Status, onSocketStatus)
socket.off(SocketEvent.Error, onSocketError)
}
}
+97 -93
View File
@@ -1,13 +1,11 @@
import {
intersection,
mergeLeft,
first,
throttleWithValue,
clamp,
sortBy,
shuffle,
pushToMapKey,
ctx,
always,
inc,
add,
ago,
@@ -35,9 +33,10 @@ import {
getCommentTags,
getPubkeyTagValues,
normalizeRelayUrl,
TrustedEvent,
Filter,
} from "@welshman/util"
import type {TrustedEvent, Filter} from "@welshman/util"
import type {RelaysAndFilters} from "@welshman/net"
import {Pool} from "@welshman/net"
import {pubkey} from "./session.js"
import {
relaySelectionsByPubkey,
@@ -46,10 +45,15 @@ import {
getWriteRelayUrls,
getRelayUrls,
} from "./relaySelections.js"
import {relays, relaysByUrl} from "./relays.js"
import {relaysByUrl} from "./relays.js"
export const INDEXED_KINDS = [PROFILE, RELAYS, INBOX_RELAYS, FOLLOWS]
export type RelaysAndFilters = {
relays: string[]
filters: Filter[]
}
export enum RelayMode {
Read = "read",
Write = "write",
@@ -75,7 +79,7 @@ export type RouterOptions = {
* Retrieves fallback relays, for use when no other relays can be selected.
* @returns An array of relay URLs as strings.
*/
getFallbackRelays: () => string[]
getFallbackRelays?: () => string[]
/**
* Retrieves relays that index profiles and relay selections.
@@ -124,8 +128,79 @@ export const addMinimalFallbacks = (count: number, limit: number) => (count > 0
export const addMaximalFallbacks = (count: number, limit: number) => limit - count
// Default router options
export const getRelayQuality = (url: string) => {
const relay = relaysByUrl.get().get(url)
// Skip non-relays entirely
if (!isRelayUrl(url)) return 0
// If we have recent errors, skip it
if (relay?.stats) {
if (relay.stats.recent_errors.filter(n => n > ago(MINUTE)).length > 0) return 0
if (relay.stats.recent_errors.filter(n => n > ago(HOUR)).length > 3) return 0
if (relay.stats.recent_errors.filter(n => n > ago(DAY)).length > 10) return 0
if (relay.stats.recent_errors.filter(n => n > ago(WEEK)).length > 50) return 0
}
// Prefer stuff we're connected to
if (Pool.getSingleton().has(url)) return 1
// Prefer stuff we've connected to in the past
if (relay?.stats) return 0.9
// If it's not weird url give it an ok score
if (!isIPAddress(url) && !isLocalUrl(url) && !isOnionUrl(url) && !url.startsWith("ws://")) {
return 0.8
}
// Default to a "meh" score
return 0.7
}
export const getPubkeyRelays = (pubkey: string, mode?: string) => {
const $relaySelections = relaySelectionsByPubkey.get()
const $inboxSelections = inboxRelaySelectionsByPubkey.get()
switch (mode) {
case RelayMode.Read:
return getReadRelayUrls($relaySelections.get(pubkey))
case RelayMode.Write:
return getWriteRelayUrls($relaySelections.get(pubkey))
case RelayMode.Inbox:
return getRelayUrls($inboxSelections.get(pubkey))
default:
return getRelayUrls($relaySelections.get(pubkey))
}
}
export const globalRouterOptions: RouterOptions = {
getRelayQuality,
getPubkeyRelays,
getFallbackRelays: () => ["wss://relay.damus.io/", "wss://nos.lol/"],
getIndexerRelays: () => ["wss://purplepag.es/", "wss://relay.nostr.band/"],
getSearchRelays: () => ["wss://relay.nostr.band/", "wss://nostr.wine/"],
getUserPubkey: () => pubkey.get(),
getLimit: () => 3,
}
// Router class
export class Router {
constructor(readonly options: RouterOptions) {}
readonly options: RouterOptions
static configure(options: RouterOptions) {
Object.assign(globalRouterOptions, options)
}
static getInstance() {
return new Router(globalRouterOptions)
}
constructor(options: RouterOptions) {
this.options = mergeLeft(options, globalRouterOptions)
}
// Utilities derived from options
@@ -152,6 +227,10 @@ export class Router {
FromRelays = (relays: string[]) => this.scenario([makeSelection(relays)])
Search = () => this.FromRelays(this.options.getSearchRelays?.() || [])
Index = () => this.FromRelays(this.options.getIndexerRelays?.() || [])
ForUser = () => this.FromRelays(this.getRelaysForUser(RelayMode.Read))
FromUser = () => this.FromRelays(this.getRelaysForUser(RelayMode.Write))
@@ -304,8 +383,7 @@ export class RouterScenario {
}
const scoreRelay = (relay: string) => {
const {getRelayQuality = always(1)} = this.router.options
const quality = getRelayQuality(relay)
const quality = this.router.options.getRelayQuality?.(relay) || 1
const weight = relayWeights.get(relay)!
// Log the weight, since it's a straight count which ends up over-weighting hubs.
@@ -320,7 +398,7 @@ export class RouterScenario {
)
const fallbacksNeeded = fallbackPolicy(relays.length, limit)
const allFallbackRelays = this.router.options.getFallbackRelays()
const allFallbackRelays: string[] = this.router.options.getFallbackRelays?.() || []
const fallbackRelays = shuffle(allFallbackRelays).slice(0, fallbacksNeeded)
for (const fallbackRelay of fallbackRelays) {
@@ -333,80 +411,6 @@ export class RouterScenario {
getUrl = () => first(this.getUrls())
}
// Default router options
export const getRelayQuality = (url: string) => {
const relay = relaysByUrl.get().get(url)
// Skip non-relays entirely
if (!isRelayUrl(url)) return 0
// If we have recent errors, skip it
if (relay?.stats) {
if (relay.stats.recent_errors.filter(n => n > ago(MINUTE)).length > 0) return 0
if (relay.stats.recent_errors.filter(n => n > ago(HOUR)).length > 3) return 0
if (relay.stats.recent_errors.filter(n => n > ago(DAY)).length > 10) return 0
if (relay.stats.recent_errors.filter(n => n > ago(WEEK)).length > 50) return 0
}
// Prefer stuff we're connected to
if (ctx.net.pool.has(url)) return 1
// Prefer stuff we've connected to in the past
if (relay?.stats) return 0.9
// If it's not weird url give it an ok score
if (!isIPAddress(url) && !isLocalUrl(url) && !isOnionUrl(url) && !url.startsWith("ws://")) {
return 0.8
}
// Default to a "meh" score
return 0.7
}
export const getPubkeyRelays = (pubkey: string, mode?: string) => {
const $relaySelections = relaySelectionsByPubkey.get()
const $inboxSelections = inboxRelaySelectionsByPubkey.get()
switch (mode) {
case RelayMode.Read:
return getReadRelayUrls($relaySelections.get(pubkey))
case RelayMode.Write:
return getWriteRelayUrls($relaySelections.get(pubkey))
case RelayMode.Inbox:
return getRelayUrls($inboxSelections.get(pubkey))
default:
return getRelayUrls($relaySelections.get(pubkey))
}
}
export const getIndexerRelays = () => ctx.app.indexerRelays || getFallbackRelays()
export const getFallbackRelays = throttleWithValue(300, () =>
sortBy(r => -getRelayQuality(r.url), relays.get())
.slice(0, 30)
.map(r => r.url),
)
export const getSearchRelays = throttleWithValue(300, () =>
sortBy(r => -getRelayQuality(r.url), relays.get())
.filter(r => r.profile?.supported_nips?.includes(50))
.slice(0, 30)
.map(r => r.url),
)
export const makeRouter = (options: Partial<RouterOptions> = {}) =>
new Router({
getPubkeyRelays,
getIndexerRelays,
getFallbackRelays,
getSearchRelays,
getRelayQuality,
getUserPubkey: () => pubkey.get(),
getLimit: () => 3,
...options,
})
// Infer relay selections from filters
type FilterScenario = {filter: Filter; scenario: RouterScenario}
@@ -416,9 +420,9 @@ type FilterSelectionRule = (filter: Filter) => FilterScenario[]
export const getFilterSelectionsForSearch = (filter: Filter) => {
if (!filter.search) return []
const relays = ctx.app.router.options.getSearchRelays?.() || []
const relays = globalRouterOptions.getSearchRelays?.() || []
return [{filter, scenario: ctx.app.router.FromRelays(relays).weight(10)}]
return [{filter, scenario: Router.getInstance().FromRelays(relays).weight(10)}]
}
export const getFilterSelectionsForWraps = (filter: Filter) => {
@@ -427,7 +431,7 @@ export const getFilterSelectionsForWraps = (filter: Filter) => {
return [
{
filter: {...filter, kinds: [WRAP]},
scenario: ctx.app.router.UserInbox(),
scenario: Router.getInstance().UserInbox(),
},
]
}
@@ -437,12 +441,12 @@ export const getFilterSelectionsForIndexedKinds = (filter: Filter) => {
if (kinds.length === 0) return []
const relays = ctx.app.router.options.getIndexerRelays?.() || []
const relays = globalRouterOptions.getIndexerRelays?.() || []
return [
{
filter: {...filter, kinds},
scenario: ctx.app.router.FromRelays(relays),
scenario: Router.getInstance().FromRelays(relays),
},
]
}
@@ -454,12 +458,12 @@ export const getFilterSelectionsForAuthors = (filter: Filter) => {
return chunks(chunkCount, filter.authors).map(authors => ({
filter: {...filter, authors},
scenario: ctx.app.router.FromPubkeys(authors),
scenario: Router.getInstance().FromPubkeys(authors),
}))
}
export const getFilterSelectionsForUser = (filter: Filter) => [
{filter, scenario: ctx.app.router.ForUser().weight(0.2)},
{filter, scenario: Router.getInstance().ForUser().weight(0.2)},
]
export const defaultFilterSelectionRules = [
@@ -489,7 +493,7 @@ export const getFilterSelections = (
const result = []
for (const [id, filter] of filtersById.entries()) {
const scenario = ctx.app.router.merge(scenariosById.get(id) || [])
const scenario = Router.getInstance().merge(scenariosById.get(id) || [])
result.push({filters: [filter], relays: scenario.getUrls()})
}
+10 -10
View File
@@ -1,18 +1,15 @@
import Fuse from "fuse.js"
import type {IFuseOptions, FuseResult} from "fuse.js"
import Fuse, {IFuseOptions, FuseResult} from "fuse.js"
import {debounce} from "throttle-debounce"
import {derived} from "svelte/store"
import {dec, sortBy} from "@welshman/lib"
import {PROFILE} from "@welshman/util"
import {PROFILE, PublishedProfile} from "@welshman/util"
import {load} from "@welshman/net"
import {throttled} from "@welshman/store"
import type {PublishedProfile} from "@welshman/util"
import {load} from "./subscribe.js"
import {wotGraph} from "./wot.js"
import {profiles} from "./profiles.js"
import {topics} from "./topics.js"
import type {Topic} from "./topics.js"
import {relays} from "./relays.js"
import type {Relay} from "./relays.js"
import {topics, Topic} from "./topics.js"
import {relays, Relay} from "./relays.js"
import {Router} from "./router.js"
import {handlesByNip05} from "./handles.js"
export type SearchOptions<V, T> = {
@@ -57,7 +54,10 @@ export const createSearch = <V, T>(options: T[], opts: SearchOptions<V, T>): Sea
export const searchProfiles = debounce(500, (search: string) => {
if (search.length > 2) {
load({filters: [{kinds: [PROFILE], search}]})
load({
filter: {kinds: [PROFILE], search},
relays: Router.getInstance().Search().getUrls(),
})
}
})
+5 -4
View File
@@ -1,10 +1,11 @@
import {openDB, deleteDB} from "idb"
import type {IDBPDatabase} from "idb"
import {IDBPDatabase} from "idb"
import {writable} from "svelte/store"
import type {Unsubscriber, Writable} from "svelte/store"
import {Unsubscriber, Writable} from "svelte/store"
import {indexBy, equals, throttle, fromPairs} from "@welshman/lib"
import type {TrustedEvent, Repository} from "@welshman/util"
import type {Tracker} from "@welshman/net"
import {TrustedEvent} from "@welshman/util"
import {Repository} from "@welshman/relay"
import {Tracker} from "@welshman/net"
import {withGetter, adapter, throttled, custom} from "@welshman/store"
export type StorageAdapterOptions = {
-57
View File
@@ -1,57 +0,0 @@
import {ctx, isNil} from "@welshman/lib"
import {LOCAL_RELAY_URL, getFilterResultCardinality} from "@welshman/util"
import type {TrustedEvent, Filter} from "@welshman/util"
import {subscribe as baseSubscribe, SubscriptionEvent} from "@welshman/net"
import type {SubscribeRequestWithHandlers} from "@welshman/net"
import {repository} from "./core.js"
export type PartialSubscribeRequest = Partial<SubscribeRequestWithHandlers> & {filters: Filter[]}
export const subscribe = (request: PartialSubscribeRequest) => {
const events: TrustedEvent[] = []
// If we already have all results for any filter, don't send the filter to the network
if (request.closeOnEose) {
for (const filter of request.filters.splice(0)) {
const cardinality = getFilterResultCardinality(filter)
if (!isNil(cardinality)) {
const results = repository.query([filter])
if (results.length === cardinality) {
for (const event of results) {
events.push(event)
}
break
}
}
request.filters.push(filter)
}
}
// Make sure to query our local relay too
const delay = ctx.app.requestDelay
const authTimeout = ctx.app.authTimeout
const timeout = request.closeOnEose ? ctx.app.requestTimeout : 0
const sub = baseSubscribe({delay, timeout, authTimeout, relays: [], ...request})
// Keep cached results async so the caller can set up handlers
setTimeout(() => {
for (const event of events) {
sub.emit(SubscriptionEvent.Event, LOCAL_RELAY_URL, event)
}
})
return sub
}
export const load = (request: PartialSubscribeRequest) =>
new Promise<TrustedEvent[]>(resolve => {
const sub = subscribe({closeOnEose: true, timeout: ctx.app.requestTimeout, ...request})
const events: TrustedEvent[] = []
sub.on(SubscriptionEvent.Event, (url: string, e: TrustedEvent) => events.push(e))
sub.on(SubscriptionEvent.Complete, () => resolve(events))
})
+26 -14
View File
@@ -1,12 +1,12 @@
import type {Filter} from "@welshman/util"
import {isSignedEvent} from "@welshman/util"
import {isSignedEvent, SignedEvent} from "@welshman/util"
import {
push as basePush,
pull as basePull,
sync as baseSync,
pushWithoutNegentropy,
pullWithoutNegentropy,
syncWithoutNegentropy,
PublishEvent,
RequestEvent,
SinglePublish,
SingleRequest,
} from "@welshman/net"
import {repository} from "./core.js"
import {relaysByUrl} from "./relays.js"
@@ -29,15 +29,23 @@ export type AppSyncOpts = {
}
export const pull = async ({relays, filters}: AppSyncOpts) => {
const events = query(filters)
const events = query(filters).filter(isSignedEvent)
await Promise.all(
relays.map(async relay => {
await (hasNegentropy(relay)
? basePull({filters, events, relays: [relay]})
: new Promise(resolve => {
new SingleRequest({filters, relay, closeOnEose: true}).on(RequestEvent.Close, resolve)
})
: Promise.all(
filters.map(
filter =>
new Promise<void>(resolve => {
new SingleRequest({filter, relay, autoClose: true}).on(
RequestEvent.Close,
resolve,
)
}),
),
))
}),
)
}
@@ -49,10 +57,14 @@ export const push = async ({relays, filters}: AppSyncOpts) => {
relays.map(async relay => {
await (hasNegentropy(relay)
? basePush({filters, events, relays: [relay]})
: new Promise(resolve => {
new SinglePublish({events, relay}).on(PublishEvent.Complete, resolve)
}))
: Promise.all(
events.map(
(event: SignedEvent) =>
new Promise<void>(resolve => {
new SinglePublish({event, relay}).on(PublishEvent.Complete, resolve)
}),
),
))
}),
)
}
+12 -11
View File
@@ -1,4 +1,4 @@
import {ctx, uniq, remove, nthEq} from "@welshman/lib"
import {uniq, remove, nthEq} from "@welshman/lib"
import {
getAddress,
isReplaceable,
@@ -9,23 +9,24 @@ import {
import type {TrustedEvent} from "@welshman/util"
import {displayProfileByPubkey} from "./profiles.js"
import {pubkey} from "./session.js"
import {Router} from "./router.js"
export const tagZapSplit = (pubkey: string, split = 1) => [
"zap",
pubkey,
ctx.app.router.FromPubkey(pubkey).getUrl(),
Router.getInstance().FromPubkey(pubkey).getUrl(),
String(split),
]
export const tagPubkey = (pubkey: string, ...args: unknown[]) => [
"p",
pubkey,
ctx.app.router.FromPubkey(pubkey).getUrl(),
Router.getInstance().FromPubkey(pubkey).getUrl(),
displayProfileByPubkey(pubkey),
]
export const tagEvent = (event: TrustedEvent, mark = "") => {
const url = ctx.app.router.Event(event).getUrl()
const url = Router.getInstance().Event(event).getUrl()
const tags = [["e", event.id, url, mark, event.pubkey]]
if (isReplaceable(event)) {
@@ -41,7 +42,7 @@ export const tagEventPubkeys = (event: TrustedEvent) =>
export const tagEventForQuote = (event: TrustedEvent) => [
"q",
event.id,
ctx.app.router.Event(event).getUrl(),
Router.getInstance().Event(event).getUrl(),
event.pubkey,
]
@@ -54,11 +55,11 @@ export const tagEventForReply = (event: TrustedEvent) => {
// Root comes first
if (roots.length > 0) {
for (const t of roots) {
tags.push([...t.slice(0, 2), ctx.app.router.EventRoots(event).getUrl(), "root"])
tags.push([...t.slice(0, 2), Router.getInstance().EventRoots(event).getUrl(), "root"])
}
} else {
for (const t of replies) {
tags.push([...t.slice(0, 2), ctx.app.router.EventParents(event).getUrl(), "root"])
tags.push([...t.slice(0, 2), Router.getInstance().EventParents(event).getUrl(), "root"])
}
}
@@ -80,7 +81,7 @@ export const tagEventForReply = (event: TrustedEvent) => {
// Finally, tag the event itself
const mark = replies.length > 0 ? "reply" : "root"
const hint = ctx.app.router.Event(event).getUrl()
const hint = Router.getInstance().Event(event).getUrl()
// e-tag the event
tags.push(["e", event.id, hint, mark, event.pubkey])
@@ -94,8 +95,8 @@ export const tagEventForReply = (event: TrustedEvent) => {
}
export const tagEventForComment = (event: TrustedEvent) => {
const pubkeyHint = ctx.app.router.FromPubkey(event.pubkey).getUrl()
const eventHint = ctx.app.router.Event(event).getUrl()
const pubkeyHint = Router.getInstance().FromPubkey(event.pubkey).getUrl()
const eventHint = Router.getInstance().Event(event).getUrl()
const address = getAddress(event)
const seenRoots = new Set<string>()
const tags: string[][] = []
@@ -129,7 +130,7 @@ export const tagEventForComment = (event: TrustedEvent) => {
}
export const tagEventForReaction = (event: TrustedEvent) => {
const hint = ctx.app.router.Event(event).getUrl()
const hint = Router.getInstance().Event(event).getUrl()
const tags: string[][] = []
// Mention the event's author
+21 -20
View File
@@ -1,24 +1,20 @@
import {writable, derived, get} from "svelte/store"
import type {Writable, Readable} from "svelte/store"
import {Worker, dissoc, identity, uniq, defer, sleep, assoc} from "@welshman/lib"
import type {Deferred} from "@welshman/lib"
import {Writable, Readable, writable, derived, get} from "svelte/store"
import {Deferred, Worker, dissoc, identity, uniq, defer, sleep, assoc} from "@welshman/lib"
import {stamp, own, hash} from "@welshman/signer"
import type {
import {
TrustedEvent,
HashedEvent,
EventTemplate,
SignedEvent,
StampedEvent,
OwnedEvent,
} from "@welshman/util"
import {
isStampedEvent,
isOwnedEvent,
isHashedEvent,
isUnwrappedEvent,
isSignedEvent,
} from "@welshman/util"
import {MultiPublish, PublishStatus} from "@welshman/net"
import {MultiPublish, PublishStatus, PublishEvent} from "@welshman/net"
import {repository, tracker} from "./core.js"
import {pubkey, getSession, getSigner} from "./session.js"
@@ -235,22 +231,27 @@ thunkWorker.addGlobalHandler((thunk: Thunk) => {
savedEvent.sig = signedEvent.sig
}
const completed = new Set()
pub.on(PublishEvent.Success, (id: string, message: string, url: string) => {
tracker.track(id, url)
thunk.status.update(assoc(url, {status: PublishStatus.Success, message}))
})
pub.emitter.on("*", async (status: PublishStatus, url: string, message = "") => {
thunk.status.update(assoc(url, {status, message}))
pub.on(PublishEvent.Failure, (id: string, message: string, url: string) => {
thunk.status.update(assoc(url, {status: PublishStatus.Failure, message}))
})
if (status !== PublishStatus.Pending) {
completed.add(url)
}
pub.on(PublishEvent.Timeout, (url: string) => {
thunk.status.update(assoc(url, {status: PublishStatus.Timeout, message: "Publish timed out"}))
})
if (status === PublishStatus.Success) {
tracker.track(signedEvent.id, url)
}
pub.on(PublishEvent.Aborted, (url: string) => {
thunk.status.update(
assoc(url, {status: PublishStatus.Aborted, message: "Publish was aborted"}),
)
})
if (completed.size === thunk.request.relays.length) {
thunk.result.resolve(get(thunk.status))
}
pub.on(PublishEvent.Complete, () => {
thunk.result.resolve(get(thunk.status))
})
})
})
+2 -2
View File
@@ -2,7 +2,6 @@ import {writable, derived} from "svelte/store"
import {Zapper} from "@welshman/util"
import {MultiRequestOptions} from "@welshman/net"
import {
ctx,
identity,
fetchJson,
uniq,
@@ -14,11 +13,12 @@ import {
} from "@welshman/lib"
import {collection} from "./collection.js"
import {deriveProfile} from "./profiles.js"
import {AppContext} from "./context.js"
export const zappers = writable<Zapper[]>([])
export const fetchZappers = async (lnurls: string[]) => {
const base = ctx.app.dufflepudUrl!
const base = AppContext.dufflepudUrl
const zappersByLnurl = new Map<string, Zapper>()
// Use dufflepud if we it's set up to protect user privacy, otherwise fetch directly
+2 -2
View File
@@ -49,8 +49,8 @@ export class DVM {
const req = new MultiRequest({relays, filter, context})
req.on(RequestEvent.Event, (e: TrustedEvent, url: string) => this.onEvent(e))
req.on(RequestEvent.Close, () => resolve())
req.on(RequestEvent.Event, this.onEvent)
req.on(RequestEvent.Close, resolve)
})
}
}
-22
View File
@@ -1,22 +0,0 @@
import type {Context} from "@welshman/lib"
/**
* A global context variable for configuring libraries and applications.
*
* In your application, you'll want to add something like the following to your types.d.ts:
* type MyContext = {
* x: number
* }
*
* declare module "@welshman/lib" {
* interface Context {
* net: MyContext
* }
* }
*/
export const ctx: Context = {}
/**
* Adds data to ctx.
*/
export const setContext = (newCtx: Context) => Object.assign(ctx, newCtx)
-5
View File
@@ -1,4 +1,3 @@
export * from "./Context.js"
export * from "./Deferred.js"
export * from "./Emitter.js"
export * from "./LRUCache.js"
@@ -6,7 +5,3 @@ export * from "./Tools.js"
export * from "./Worker.js"
export * from "./TaskQueue.js"
export {default as normalizeUrl} from "./normalize-url/index.js"
declare module "@welshman/lib" {
export interface Context {}
}
+1 -1
View File
@@ -207,7 +207,7 @@ export const pull = async ({context, ...options}: PullOptions) => {
const req = new SingleRequest({relay, context, filter: {ids}, autoClose: true})
req.on(RequestEvent.Close, resolve)
req.on(RequestEvent.Event, event => result.push(event))
req.on(RequestEvent.Event, event => result.push(event as SignedEvent))
})
}),
)
+38 -15
View File
@@ -1,14 +1,14 @@
import {EventEmitter} from "events"
import {verifyEvent as nostrToolsVerifyEvent} from "nostr-tools/pure"
import {on, call, randomId, yieldThread} from "@welshman/lib"
import {Filter, matchFilter, SignedEvent} from "@welshman/util"
import {Filter, matchFilter, TrustedEvent, getFilterResultCardinality} from "@welshman/util"
import {RelayMessage, ClientMessageType, isRelayEvent, isRelayEose} from "./message.js"
import {getAdapter, AdapterContext, AbstractAdapter, AdapterEvent} from "./adapter.js"
import {SocketEvent, SocketStatus} from "./socket.js"
import {TypedEmitter, Unsubscriber} from "./util.js"
import {Tracker} from "./tracker.js"
export const defaultVerifyEvent = (event: SignedEvent) => {
export const defaultVerifyEvent = (event: any) => {
try {
return nostrToolsVerifyEvent(event)
} catch (e) {
@@ -29,10 +29,10 @@ export enum RequestEvent {
// SingleRequest
export type SingleRequestEvents = {
[RequestEvent.Event]: (event: SignedEvent) => void
[RequestEvent.Invalid]: (event: SignedEvent) => void
[RequestEvent.Filtered]: (event: SignedEvent) => void
[RequestEvent.Duplicate]: (event: SignedEvent) => void
[RequestEvent.Event]: (event: TrustedEvent) => void
[RequestEvent.Invalid]: (event: any) => void
[RequestEvent.Filtered]: (event: TrustedEvent) => void
[RequestEvent.Duplicate]: (event: TrustedEvent) => void
[RequestEvent.Disconnect]: () => void
[RequestEvent.Close]: () => void
[RequestEvent.Eose]: () => void
@@ -45,7 +45,7 @@ export type SingleRequestOptions = {
timeout?: number
tracker?: Tracker
autoClose?: boolean
verifyEvent?: (event: SignedEvent) => boolean
verifyEvent?: (event: any) => boolean
}
export class SingleRequest extends (EventEmitter as new () => TypedEmitter<SingleRequestEvents>) {
@@ -138,10 +138,10 @@ export class SingleRequest extends (EventEmitter as new () => TypedEmitter<Singl
// MultiRequest
export type MultiRequestEvents = {
[RequestEvent.Event]: (event: SignedEvent, url: string) => void
[RequestEvent.Invalid]: (event: SignedEvent, url: string) => void
[RequestEvent.Filtered]: (event: SignedEvent, url: string) => void
[RequestEvent.Duplicate]: (event: SignedEvent, url: string) => void
[RequestEvent.Event]: (event: TrustedEvent, url: string) => void
[RequestEvent.Invalid]: (event: TrustedEvent, url: string) => void
[RequestEvent.Filtered]: (event: TrustedEvent, url: string) => void
[RequestEvent.Duplicate]: (event: TrustedEvent, url: string) => void
[RequestEvent.Disconnect]: (url: string) => void
[RequestEvent.Eose]: (url: string) => void
[RequestEvent.Close]: () => void
@@ -163,19 +163,19 @@ export class MultiRequest extends (EventEmitter as new () => TypedEmitter<MultiR
for (const relay of relays) {
const req = new SingleRequest({relay, tracker, ...options})
req.on(RequestEvent.Event, (event: SignedEvent) => {
req.on(RequestEvent.Event, (event: TrustedEvent) => {
this.emit(RequestEvent.Event, event, relay)
})
req.on(RequestEvent.Invalid, (event: SignedEvent) => {
req.on(RequestEvent.Invalid, (event: TrustedEvent) => {
this.emit(RequestEvent.Invalid, event, relay)
})
req.on(RequestEvent.Filtered, (event: SignedEvent) => {
req.on(RequestEvent.Filtered, (event: TrustedEvent) => {
this.emit(RequestEvent.Filtered, event, relay)
})
req.on(RequestEvent.Duplicate, (event: SignedEvent) => {
req.on(RequestEvent.Duplicate, (event: TrustedEvent) => {
this.emit(RequestEvent.Duplicate, event, relay)
})
@@ -205,3 +205,26 @@ export class MultiRequest extends (EventEmitter as new () => TypedEmitter<MultiR
}
}
}
/**
* A convenience function which returns a promise of events from a request.
* It may return early if filter cardinality is known.
* @param options - MultiRequestOptions
* @returns - a promise containing an array of TrustedEvents
*/
export const load = (options: MultiRequestOptions) =>
new Promise(resolve => {
const cardinality = getFilterResultCardinality(options.filter)
const req = new MultiRequest({timeout: 5000, ...options, autoClose: true})
const events: TrustedEvent[] = []
req.on(RequestEvent.Event, (event: TrustedEvent) => {
events.push(event)
if (events.length === cardinality) {
resolve(events)
}
})
req.on(RequestEvent.Close, () => resolve(events))
})