Skip to content

Query System ​

How to build, send, and subscribe to Nostr queries using RequestBuilder, Query, and NoteCollection.

RequestBuilder ​

Fluent API for constructing Nostr filter requests.

Constructor ​

typescript
const rb = new RequestBuilder('unique-id')

The ID should be unique to the use case. All data fetched with the same ID is merged into the same store.

Adding Filters ​

withFilter(): RequestFilterBuilder ​

Add a new filter. Multiple filters use OR logic.

typescript
const rb = new RequestBuilder('feed')
  .withFilter()
    .kinds([EventKind.TextNote])
    .authors(['pubkey1', 'pubkey2'])
    .limit(50)
    .since(Math.floor(Date.now() / 1000) - 3600)

withBareFilter(f: ReqFilter): RequestFilterBuilder ​

Add a raw filter object.

typescript
rb.withBareFilter({ kinds: [1], limit: 10 })

Options ​

withOptions(opt: RequestBuilderOptions): this ​

OptionTypeDescription
leaveOpenbooleanDon't send CLOSE after EOSE, keep streaming
outboxPickNnumberPick N relays per pubkey with outbox model
groupingDelaynumberWait time (ms) to group similar requests
replaceablebooleanReplace query when changes detected (e.g. live chat)
skipCachebooleanSkip the cache relay
useSyncModulebooleanEnable negentropy/range-sync
extraEventsNostrEvent[]Extra events to include in the store
typescript
rb.withOptions({
  leaveOpen: true,
  replaceable: true,
  outboxPickN: 3,
})

Relay Targeting ​

withRelays(relays: string[]): this ​

Force all filters to use specific relays.

typescript
rb.withRelays(['wss://relay.snort.social', 'wss://nos.lol'])

Combining Builders ​

add(other: RequestBuilder): void ​

Merge another builder's filters into this one.

typescript
const rb1 = new RequestBuilder('part1')
rb1.withFilter().authors(['pubkey1']).kinds([1])

const rb2 = new RequestBuilder('part2')
rb2.withFilter().authors(['pubkey2']).kinds([1])

rb1.add(rb2)

RequestFilterBuilder ​

Builder for individual filters within a request.

Methods ​

relay(url: string | string[]): this ​

Target specific relay(s) for this filter.

typescript
rb.withFilter().relay('wss://relay.example.com').kinds([1])

ids(ids: string[]): this ​

Filter by event IDs.

typescript
rb.withFilter().ids(['event-id-1', 'event-id-2'])

authors(authors: string[]): this ​

Filter by pubkeys (hex, 64 chars).

typescript
rb.withFilter().authors(['hex-pubkey-1', 'hex-pubkey-2'])

kinds(kinds: EventKind[]): this ​

Filter by event kinds.

typescript
rb.withFilter().kinds([EventKind.TextNote, EventKind.LongFormTextNote])

since(timestamp: number): this ​

Filter events created after timestamp.

typescript
rb.withFilter().since(Math.floor(Date.now() / 1000) - 86400) // last 24h

until(timestamp: number): this ​

Filter events created before timestamp.

typescript
rb.withFilter().until(Math.floor(Date.now() / 1000))

limit(count: number): this ​

Limit number of results.

typescript
rb.withFilter().limit(50)

tag(key: string, values: string[]): this ​

Filter by tag. Supports e, p, d, t, r, a, g, and any custom tag key.

typescript
rb.withFilter().tag('e', ['event-id'])        // Events referencing event-id
rb.withFilter().tag('p', ['pubkey'])           // Events referencing pubkey
rb.withFilter().tag('d', ['identifier'])       // Replaceable event identifier
rb.withFilter().tag('t', ['bitcoin', 'nostr']) // Hashtags

tags(tags: ToNostrEventTag[]): this ​

Add multiple tag filters from ToNostrEventTag objects.

typescript
const links = [NostrLink.publicKey('pubkey1'), NostrLink.publicKey('pubkey2')]
rb.withFilter().tags(links)

search(keyword: string): this ​

Full-text search (requires relay support, NIP-50).

typescript
rb.withFilter().search('bitcoin lightning')

Filter by a NostrLink or ToNostrEventTag. Handles note, event, profile, and address links.

typescript
const link = NostrLink.fromEvent(event)
rb.withFilter().link(link)

Filter for replies to the given links (using e or a tags).

typescript
const rootLink = NostrLink.fromEvent(rootEvent)
rb.withFilter().replyToLink([rootLink])

Query ​

When you call System.Query(rb), you get a QueryLike object.

Properties ​

PropertyTypeDescription
progressnumberQuery progress (0-1)
snapshotTaggedNostrEvent[]Current event snapshot

Methods ​

MethodDescription
cancel()Mark query for cancellation
uncancel()Un-mark query for cancellation
start()Start the query request flow
flush()Flush buffered data to listeners

Events ​

EventCallbackDescription
event(events: TaggedNostrEvent[]) => voidNew events received (batched ~300ms)
eot() => voidEnd of stored events (all relays EOSE)

Example ​

typescript
const rb = new RequestBuilder('profile')
  .withFilter().kinds([EventKind.SetMetadata]).authors(['pubkey1'])

const q = System.Query(rb)

// Listen for events
q.on('event', (events) => {
  events.forEach(ev => {
    const profile = JSON.parse(ev.content)
    console.log(profile.name, profile.about)
  })
})

// Listen for completion
q.on('eot', () => {
  console.log('All stored events loaded')
})

// Cancel when done
setTimeout(() => q.cancel(), 10000)

NoteCollection ​

Event store that manages received events with deduplication and sorting.

Types ​

typescript
type NoteStoreSnapshotData = Array<TaggedNostrEvent>

Built-in Collections ​

The Query uses a NoteCollection internally which:

  • Deduplicates events by ID
  • Sorts by created_at descending
  • Handles replaceable events (keeps latest)
  • Handles parameterized replaceable events (keeps latest per d tag)
  • Buffers emissions (~300ms) to reduce re-renders

QueryManager ​

Internal system that manages all active queries. Access via System methods.

Query Trace States ​

typescript
enum QueryTraceState {
  NEW = "NEW",             // Not yet used
  QUEUED = "QUEUED",       // First created
  WAITING = "WAITING",     // Sent REQ, will close on EOSE
  WAITING_STREAM = "WAITING_STREAM", // Streaming, stay open after EOSE
  SYNC_WAITING = "SYNC_WAITING",     // Waiting for NEG-OPEN response
  SYNC_FALLBACK = "SYNC_FALLBACK",   // SYNC not supported, fallback to REQ
  EOSE = "EOSE",           // Server reported end of stored events
  LOCAL_CLOSE = "LOCAL_CLOSE",       // We sent close
  REMOTE_CLOSE = "REMOTE_CLOSE",     // Server closed the request
  DROP = "DROP",           // Dropped due to disconnect
  TIMEOUT = "TIMEOUT",     // Closed due to timeout
}

Complete Example ​

typescript
import { NostrSystem, RequestBuilder, EventKind } from '@snort/system'

const System = new NostrSystem({})
await System.Init()
await System.ConnectToRelay('wss://relay.snort.social', { read: true })

// Build a complex query
const rb = new RequestBuilder('home-timeline')
  .withOptions({ leaveOpen: true })

// Follows' text notes
rb.withFilter()
  .authors(followList)
  .kinds([EventKind.TextNote])
  .since(lastCheck)
  .limit(100)

// Global trending (separate filter = OR logic)
rb.withFilter()
  .kinds([EventKind.TextNote])
  .tag('t', ['nostr'])
  .limit(20)

const q = System.Query(rb)

q.on('event', (events) => {
  // Called every ~300ms with new events
  updateFeed(events)
})

q.on('eot', () => {
  // All relays have reported EOSE
  setLoading(false)
})

See Also ​