Fix ts errors
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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()})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)])
|
||||
|
||||
@@ -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})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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})
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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(),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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 {}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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))
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user