Accept multiple filters to request

This commit is contained in:
Jon Staab
2025-04-01 13:15:50 -07:00
parent fd0cdf2c19
commit 05a9d6461b
16 changed files with 105 additions and 93 deletions
+3 -4
View File
@@ -7681,7 +7681,7 @@
"@welshman/lib": "^0.1.0", "@welshman/lib": "^0.1.0",
"@welshman/net": "^0.0.49", "@welshman/net": "^0.0.49",
"@welshman/relay": "^0.1.0", "@welshman/relay": "^0.1.0",
"@welshman/signer": "^0.1.0", "@welshman/signer": "^0.1.1",
"@welshman/store": "^0.1.0", "@welshman/store": "^0.1.0",
"@welshman/util": "^0.1.0", "@welshman/util": "^0.1.0",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
@@ -7771,8 +7771,8 @@
"@noble/hashes": "^1.6.1", "@noble/hashes": "^1.6.1",
"@welshman/lib": "^0.1.0", "@welshman/lib": "^0.1.0",
"@welshman/net": "^0.0.49", "@welshman/net": "^0.0.49",
"@welshman/util": "^0.1.0", "@welshman/signer": "^0.1.1",
"nostr-tools": "^2.7.2" "@welshman/util": "^0.1.0"
} }
}, },
"packages/dvm/node_modules/@noble/hashes": { "packages/dvm/node_modules/@noble/hashes": {
@@ -7860,7 +7860,6 @@
"@welshman/relay": "^0.1.0", "@welshman/relay": "^0.1.0",
"@welshman/util": "^0.1.0", "@welshman/util": "^0.1.0",
"isomorphic-ws": "^5.0.0", "isomorphic-ws": "^5.0.0",
"nostr-tools": "^2.11.0",
"typed-emitter": "^2.1.0" "typed-emitter": "^2.1.0"
} }
}, },
+1 -1
View File
@@ -32,7 +32,7 @@
"@welshman/lib": "^0.1.0", "@welshman/lib": "^0.1.0",
"@welshman/relay": "^0.1.0", "@welshman/relay": "^0.1.0",
"@welshman/net": "^0.0.49", "@welshman/net": "^0.0.49",
"@welshman/signer": "^0.1.0", "@welshman/signer": "^0.1.1",
"@welshman/store": "^0.1.0", "@welshman/store": "^0.1.0",
"@welshman/util": "^0.1.0", "@welshman/util": "^0.1.0",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
+4
View File
@@ -3,12 +3,16 @@ import {verifyEvent, isEphemeralKind, isDVMKind} from "@welshman/util"
import {Repository} from "@welshman/relay" import {Repository} from "@welshman/relay"
import {Pool, Tracker, SocketEvent, isRelayEvent} from "@welshman/net" import {Pool, Tracker, SocketEvent, isRelayEvent} from "@welshman/net"
import {custom} from "@welshman/store" import {custom} from "@welshman/store"
import {loadRelay, trackRelayStats} from "./relays.js"
export const repository = Repository.getSingleton() export const repository = Repository.getSingleton()
export const tracker = new Tracker() export const tracker = new Tracker()
Pool.getSingleton().subscribe(socket => { Pool.getSingleton().subscribe(socket => {
loadRelay(socket.url)
trackRelayStats(socket)
socket.on(SocketEvent.Receive, message => { socket.on(SocketEvent.Receive, message => {
if (isRelayEvent(message)) { if (isRelayEvent(message)) {
const event = message[2] const event = message[2]
+5 -10
View File
@@ -11,17 +11,12 @@ import {wotGraph, maxWot, getFollows, getNetwork, getFollowers} from "./wot.js"
export const request = async ({filters = [{}], relays = [], onEvent}: RequestOpts) => { export const request = async ({filters = [{}], relays = [], onEvent}: RequestOpts) => {
if (relays.length > 0) { if (relays.length > 0) {
await Promise.all( await new Promise<void>(resolve => {
filters.map( const sub = new MultiRequest({filters, relays, timeout: 5000, autoClose: true})
filter =>
new Promise<void>(resolve => {
const sub = new MultiRequest({filter, relays, timeout: 5000, autoClose: true})
sub.on(RequestEvent.Event, onEvent) sub.on(RequestEvent.Event, onEvent)
sub.on(RequestEvent.Close, resolve) sub.on(RequestEvent.Close, resolve)
}), })
),
)
} else { } else {
await Promise.all(getFilterSelections(filters).map(opts => request({...opts, onEvent}))) await Promise.all(getFilterSelections(filters).map(opts => request({...opts, onEvent})))
} }
+2 -2
View File
@@ -24,9 +24,9 @@ export const {
load: async (pubkey: string, request: Partial<MultiRequestOptions> = {}) => { load: async (pubkey: string, request: Partial<MultiRequestOptions> = {}) => {
await loadRelaySelections(pubkey, request) await loadRelaySelections(pubkey, request)
const filter = {kinds: [FOLLOWS], authors: [pubkey]} const filters = [{kinds: [FOLLOWS], authors: [pubkey]}]
const relays = Router.get().FromPubkey(pubkey).getUrls() const relays = Router.get().FromPubkey(pubkey).getUrls()
await load({relays, ...request, filter}) await load({relays, ...request, filters})
}, },
}) })
+2 -2
View File
@@ -30,9 +30,9 @@ export const {
load: async (pubkey: string, request: Partial<MultiRequestOptions> = {}) => { load: async (pubkey: string, request: Partial<MultiRequestOptions> = {}) => {
await loadRelaySelections(pubkey, request) await loadRelaySelections(pubkey, request)
const filter = {kinds: [MUTES], authors: [pubkey]} const filters = [{kinds: [MUTES], authors: [pubkey]}]
const relays = Router.get().FromPubkey(pubkey).getUrls() const relays = Router.get().FromPubkey(pubkey).getUrls()
await load({relays, ...request, filter}) await load({relays, ...request, filters})
}, },
}) })
+2 -2
View File
@@ -24,9 +24,9 @@ export const {
load: async (pubkey: string, request: Partial<MultiRequestOptions> = {}) => { load: async (pubkey: string, request: Partial<MultiRequestOptions> = {}) => {
await loadRelaySelections(pubkey, request) await loadRelaySelections(pubkey, request)
const filter = {kinds: [PINS], authors: [pubkey]} const filters = [{kinds: [PINS], authors: [pubkey]}]
const relays = Router.get().FromPubkey(pubkey).getUrls() const relays = Router.get().FromPubkey(pubkey).getUrls()
await load({relays, ...request, filter}) await load({relays, ...request, filters})
}, },
}) })
+2 -2
View File
@@ -28,10 +28,10 @@ export const {
await loadRelaySelections(pubkey, request) await loadRelaySelections(pubkey, request)
const router = Router.get() const router = Router.get()
const filter = {kinds: [PROFILE], authors: [pubkey]} const filters = [{kinds: [PROFILE], authors: [pubkey]}]
const relays = router.merge([router.Index(), router.FromPubkey(pubkey)]).getUrls() const relays = router.merge([router.Index(), router.FromPubkey(pubkey)]).getUrls()
await load({relays, ...request, filter}) await load({relays, ...request, filters})
}, },
}) })
+2 -2
View File
@@ -53,7 +53,7 @@ export const {
await load({ await load({
relays: router.merge([router.Index(), router.FromPubkey(pubkey)]).getUrls(), relays: router.merge([router.Index(), router.FromPubkey(pubkey)]).getUrls(),
...request, ...request,
filter: {kinds: [RELAYS], authors: [pubkey]}, filters: [{kinds: [RELAYS], authors: [pubkey]}],
}) })
}, },
}) })
@@ -78,7 +78,7 @@ export const {
await load({ await load({
relays: router.merge([router.Index(), router.FromPubkey(pubkey)]).getUrls(), relays: router.merge([router.Index(), router.FromPubkey(pubkey)]).getUrls(),
...request, ...request,
filter: {kinds: [INBOX_RELAYS], authors: [pubkey]}, filters: [{kinds: [INBOX_RELAYS], authors: [pubkey]}],
}) })
}, },
}) })
+1 -1
View File
@@ -55,7 +55,7 @@ export const createSearch = <V, T>(options: T[], opts: SearchOptions<V, T>): Sea
export const searchProfiles = debounce(500, (search: string) => { export const searchProfiles = debounce(500, (search: string) => {
if (search.length > 2) { if (search.length > 2) {
load({ load({
filter: {kinds: [PROFILE], search}, filters: [{kinds: [PROFILE], search}],
relays: Router.get().Search().getUrls(), relays: Router.get().Search().getUrls(),
}) })
} }
+3 -11
View File
@@ -35,17 +35,9 @@ export const pull = async ({relays, filters}: AppSyncOpts) => {
relays.map(async relay => { relays.map(async relay => {
await (hasNegentropy(relay) await (hasNegentropy(relay)
? basePull({filters, events, relays: [relay]}) ? basePull({filters, events, relays: [relay]})
: Promise.all( : new Promise<void>(resolve => {
filters.map( new SingleRequest({filters, relay, autoClose: true}).on(RequestEvent.Close, resolve)
filter => }))
new Promise<void>(resolve => {
new SingleRequest({filter, relay, autoClose: true}).on(
RequestEvent.Close,
resolve,
)
}),
),
))
}), }),
) )
} }
+1 -1
View File
@@ -50,7 +50,7 @@ export class DVM {
filter["#p"] = [pubkey] filter["#p"] = [pubkey]
} }
const req = new MultiRequest({relays, filter, context}) const req = new MultiRequest({relays, filters: [filter], context})
req.on(RequestEvent.Event, this.onEvent) req.on(RequestEvent.Event, this.onEvent)
req.on(RequestEvent.Close, resolve) req.on(RequestEvent.Close, resolve)
+2 -2
View File
@@ -35,9 +35,9 @@ export const makeDvmRequest = (request: DVMRequestOptions) => {
} = request } = request
const kind = event.kind + 1000 const kind = event.kind + 1000
const kinds = reportProgress ? [kind, 7000] : [kind] const kinds = reportProgress ? [kind, 7000] : [kind]
const filter: Filter = {kinds, since: now() - 60, "#e": [event.id]} const filters: Filter[] = [{kinds, since: now() - 60, "#e": [event.id]}]
const sub = new MultiRequest({relays, filter, timeout, context}) const sub = new MultiRequest({relays, filters, timeout, context})
const pub = new MultiPublish({relays, event, timeout, context}) const pub = new MultiPublish({relays, event, timeout, context})
sub.on(RequestEvent.Event, (event: TrustedEvent, url: string) => { sub.on(RequestEvent.Event, (event: TrustedEvent, url: string) => {
+1 -1
View File
@@ -204,7 +204,7 @@ export const pull = async ({context, ...options}: PullOptions) => {
return Promise.all( return Promise.all(
chunk(500, allIds).map(ids => { chunk(500, allIds).map(ids => {
return new Promise<void>(resolve => { return new Promise<void>(resolve => {
const req = new SingleRequest({relay, context, filter: {ids}, autoClose: true}) const req = new SingleRequest({relay, context, filters: [{ids}], autoClose: true})
req.on(RequestEvent.Close, resolve) req.on(RequestEvent.Close, resolve)
req.on(RequestEvent.Event, event => result.push(event as SignedEvent)) req.on(RequestEvent.Event, event => result.push(event as SignedEvent))
+72 -50
View File
@@ -3,7 +3,7 @@ import {on, call, randomId, yieldThread, pushToMapKey, batcher} from "@welshman/
import { import {
Filter, Filter,
unionFilters, unionFilters,
matchFilter, matchFilters,
TrustedEvent, TrustedEvent,
getFilterResultCardinality, getFilterResultCardinality,
} from "@welshman/util" } from "@welshman/util"
@@ -40,7 +40,7 @@ export type SingleRequestEvents = {
export type SingleRequestOptions = { export type SingleRequestOptions = {
relay: string relay: string
filter: Filter filters: Filter[]
context?: AdapterContext context?: AdapterContext
timeout?: number timeout?: number
tracker?: Tracker tracker?: Tracker
@@ -49,8 +49,12 @@ export type SingleRequestOptions = {
isEventDeleted?: (event: TrustedEvent, url: string) => boolean isEventDeleted?: (event: TrustedEvent, url: string) => boolean
} }
// Needed for typescript to infer emitter methods
export interface SingleRequest extends TypedEmitter<SingleRequestEvents> {}
export class SingleRequest extends (EventEmitter as new () => TypedEmitter<SingleRequestEvents>) { export class SingleRequest extends (EventEmitter as new () => TypedEmitter<SingleRequestEvents>) {
_id = `REQ-${randomId().slice(0, 8)}` _ids = new Set<string>()
_eose = new Set<string>()
_unsubscribers: Unsubscriber[] = [] _unsubscribers: Unsubscriber[] = []
_adapter: AbstractAdapter _adapter: AbstractAdapter
_closed = false _closed = false
@@ -71,29 +75,33 @@ export class SingleRequest extends (EventEmitter as new () => TypedEmitter<Singl
if (isRelayEvent(message)) { if (isRelayEvent(message)) {
const [_, id, event] = message const [_, id, event] = message
if (id !== this._id) return if (this._ids.has(id)) {
if (tracker.track(event.id, url)) {
if (tracker.track(event.id, url)) { this.emit(RequestEvent.Duplicate, event)
this.emit(RequestEvent.Duplicate, event) } else if (isEventDeleted(event, url)) {
} else if (isEventDeleted(event, url)) { this.emit(RequestEvent.Deleted, event)
this.emit(RequestEvent.Deleted, event) } else if (!isEventValid(event, url)) {
} else if (!isEventValid(event, url)) { this.emit(RequestEvent.Invalid, event)
this.emit(RequestEvent.Invalid, event) } else if (!matchFilters(this.options.filters, event)) {
} else if (!matchFilter(this.options.filter, event)) { this.emit(RequestEvent.Filtered, event)
this.emit(RequestEvent.Filtered, event) } else {
} else { this.emit(RequestEvent.Event, event)
this.emit(RequestEvent.Event, event) }
} }
} }
if (isRelayEose(message)) { if (isRelayEose(message)) {
const [_, id] = message const [_, id] = message
if (id === this._id) { if (this._ids.has(id)) {
this.emit(RequestEvent.Eose) this._eose.add(id)
if (this.options.autoClose) { if (this._eose.size === this._ids.size) {
this.close() this.emit(RequestEvent.Eose)
if (this.options.autoClose) {
this.close()
}
} }
} }
} }
@@ -122,14 +130,22 @@ export class SingleRequest extends (EventEmitter as new () => TypedEmitter<Singl
// Start asynchronously so the caller can set up listeners // Start asynchronously so the caller can set up listeners
yieldThread().then(() => { yieldThread().then(() => {
this._adapter.send([ClientMessageType.Req, this._id, this.options.filter]) for (const filter of this.options.filters) {
const id = `REQ-${randomId().slice(0, 8)}`
this._ids.add(id)
this._adapter.send([ClientMessageType.Req, id, filter])
}
}) })
} }
close() { close() {
if (this._closed) return if (this._closed) return
this._adapter.send(["CLOSE", this._id]) for (const id of this._ids) {
this._adapter.send(["CLOSE", id])
}
this.emit(RequestEvent.Close) this.emit(RequestEvent.Close)
this.removeAllListeners() this.removeAllListeners()
this._unsubscribers.map(call) this._unsubscribers.map(call)
@@ -155,6 +171,9 @@ export type MultiRequestOptions = Omit<SingleRequestOptions, "relay"> & {
relays: string[] relays: string[]
} }
// Needed for typescript to infer emitter methods
export interface MultiRequest extends TypedEmitter<MultiRequestEvents> {}
export class MultiRequest extends (EventEmitter as new () => TypedEmitter<MultiRequestEvents>) { export class MultiRequest extends (EventEmitter as new () => TypedEmitter<MultiRequestEvents>) {
_children: SingleRequest[] = [] _children: SingleRequest[] = []
_closed = new Set<string>() _closed = new Set<string>()
@@ -214,6 +233,8 @@ export class MultiRequest extends (EventEmitter as new () => TypedEmitter<MultiR
} }
} }
export const request = (options: MultiRequestOptions) => new MultiRequest(options)
/** /**
* A convenience function which returns a promise of events from a request. * A convenience function which returns a promise of events from a request.
* It may return early if filter cardinality is known, and it delays requests by * It may return early if filter cardinality is known, and it delays requests by
@@ -224,9 +245,11 @@ export class MultiRequest extends (EventEmitter as new () => TypedEmitter<MultiR
export const load = batcher(200, async (requests: MultiRequestOptions[]) => { export const load = batcher(200, async (requests: MultiRequestOptions[]) => {
const filtersByRelay = new Map<string, Filter[]>() const filtersByRelay = new Map<string, Filter[]>()
for (const {filter, relays} of requests) { for (const {filters, relays} of requests) {
for (const relay of relays) { for (const relay of relays) {
pushToMapKey(filtersByRelay, relay, filter) for (const filter of filters) {
pushToMapKey(filtersByRelay, relay, filter)
}
} }
} }
@@ -234,35 +257,34 @@ export const load = batcher(200, async (requests: MultiRequestOptions[]) => {
const events: TrustedEvent[] = [] const events: TrustedEvent[] = []
await Promise.all( await Promise.all(
Array.from(filtersByRelay).map(async ([relay, filters]) => { Array.from(filtersByRelay).map(
await Promise.all( async ([relay, unmergedFilters]) =>
unionFilters(filters).map(filter => { new Promise<void>(resolve => {
new Promise<void>(resolve => { const filters = unionFilters(unmergedFilters)
const cardinality = getFilterResultCardinality(filter) const cardinality =
const req = new MultiRequest({ filters.length === 1 ? getFilterResultCardinality(filters[0]) : undefined
filter, const req = new MultiRequest({
tracker, filters,
relays: [relay], tracker,
timeout: 5000, relays: [relay],
autoClose: true, timeout: 5000,
}) autoClose: true,
let count = 0
req.on(RequestEvent.Event, (event: TrustedEvent) => {
events.push(event)
if (++count === cardinality) {
resolve()
}
})
req.on(RequestEvent.Close, () => resolve())
}) })
let count = 0
req.on(RequestEvent.Event, (event: TrustedEvent) => {
events.push(event)
if (++count === cardinality) {
resolve()
}
})
req.on(RequestEvent.Close, () => resolve())
}), }),
) ),
}),
) )
return requests.map(r => events.filter(event => matchFilter(r.filter, event))) return requests.map(r => events.filter(event => matchFilters(r.filters, event)))
}) })
+2 -2
View File
@@ -112,9 +112,9 @@ export class Nip46Receiver extends Emitter {
const {relays, context} = this.params const {relays, context} = this.params
const userPubkey = await this.signer.getPubkey() const userPubkey = await this.signer.getPubkey()
const filter = {kinds: [NOSTR_CONNECT], "#p": [userPubkey]} const filters = [{kinds: [NOSTR_CONNECT], "#p": [userPubkey]}]
this.sub = new MultiRequest({relays, filter, context}) this.sub = new MultiRequest({relays, filters, context})
this.sub.on(RequestEvent.Event, async (event: TrustedEvent, url: string) => { this.sub.on(RequestEvent.Event, async (event: TrustedEvent, url: string) => {
const json = await decrypt(this.signer, event.pubkey, event.content) const json = await decrypt(this.signer, event.pubkey, event.content)