13 KiB
| name | description |
|---|---|
| welshman-feeds | Use this skill when working with @welshman/feeds: building nostr feeds, FeedController, FeedCompiler, feed definitions, dynamic filtering, or composing feed sources. |
welshman/feeds — Dynamic Feed Construction
@welshman/feeds provides a declarative, composable system for defining and executing Nostr event feeds. You describe what you want (authors, kinds, tags, set operations) as a data structure, and the package compiles it into optimized relay requests and handles pagination, deduplication, and exhaustion. It sits on top of @welshman/net for relay communication and @welshman/util for types.
Installation
npm install @welshman/feeds
# pnpm add @welshman/feeds
# yarn add @welshman/feeds
Key Exports
Feed Types (enum + tuple types)
| Export | Description |
|---|---|
FeedType |
Enum of all feed type discriminants (Author, Kind, Tag, Union, Intersection, Difference, DVM, List, Label, WOT, Scope, Relay, Search, ID, Address, CreatedAt, Global) |
Scope |
Enum: Followers, Follows, Network, Self |
Feed |
Union type of all feed tuple types |
RequestItem |
{ relays?: string[], filters?: Filter[] } — output of compilation |
Factory Functions
All feed definitions are typed tuples. Always use factories rather than raw arrays.
// Leaf feeds
makeAuthorFeed(...pubkeys: string[]): AuthorFeed
makeKindFeed(...kinds: number[]): KindFeed
makeTagFeed(key: string, ...values: string[]): TagFeed
makeIDFeed(...ids: string[]): IDFeed
makeAddressFeed(...addresses: string[]): AddressFeed
makeRelayFeed(...urls: string[]): RelayFeed
makeSearchFeed(...terms: string[]): SearchFeed
makeGlobalFeed(): GlobalFeed
makeScopeFeed(...scopes: Scope[]): ScopeFeed
makeWOTFeed(...items: WOTItem[]): WOTFeed
makeCreatedAtFeed(...items: CreatedAtItem[]): CreatedAtFeed
// Dynamic / remote feeds
makeDVMFeed(...items: DVMItem[]): DVMFeed
makeListFeed(...items: ListItem[]): ListFeed
makeLabelFeed(...items: LabelItem[]): LabelFeed
// Set operations
makeUnionFeed(...feeds: Feed[]): UnionFeed
makeIntersectionFeed(...feeds: Feed[]): IntersectionFeed
makeDifferenceFeed(...feeds: Feed[]): DifferenceFeed
Type Guards
isAuthorFeed(feed) isKindFeed(feed) isTagFeed(feed)
isIDFeed(feed) isAddressFeed(feed) isRelayFeed(feed)
isSearchFeed(feed) isGlobalFeed(feed) isScopeFeed(feed)
isWOTFeed(feed) isCreatedAtFeed(feed)
isDVMFeed(feed) isListFeed(feed) isLabelFeed(feed)
isUnionFeed(feed) isIntersectionFeed(feed) isDifferenceFeed(feed)
hasSubFeeds(feed) // true for Union | Intersection | Difference
Argument Extraction
getFeedArgs(feed: AuthorFeed): string[]
getFeedArgs(feed: KindFeed): number[]
getFeedArgs(feed: CreatedAtFeed): CreatedAtItem[]
getFeedArgs(feed: WOTFeed): WOTItem[]
getFeedArgs(feed: UnionFeed): Feed[]
// overloaded for every feed type
Conversion Utilities
// Tags → feeds
feedsFromTags(tags: string[][], mappings?: TagFeedMapping[]): Feed[]
feedFromTags(tags: string[][], mappings?: TagFeedMapping[]): IntersectionFeed
// Filter(s) → feeds
feedsFromFilter(filter: Filter): Feed[]
feedFromFilter(filter: Filter): Feed
feedFromFilters(filters: Filter[]): Feed
// Default tag-to-feed mappings (override via DVMItem/ListItem mappings)
defaultTagFeedMappings: TagFeedMapping[]
// [["a", Address], ["e", ID], ["p", Author], ["r", Relay], ["t", Tag "#t"]]
Traversal & Simplification
walkFeed(feed: Feed, visit: (feed: Feed) => void): void
findFeed(feed: Feed, match: (feed: Feed) => boolean): Feed | undefined
simplifyFeed(feed: Feed): Feed // flattens nested same-type set ops
FeedCompiler
Transforms a Feed into RequestItem[] for direct relay querying.
class FeedCompiler {
constructor(options: FeedCompilerOptions)
canCompile(feed: Feed): boolean
async compile(feed: Feed): Promise<RequestItem[]>
}
type FeedCompilerOptions = {
router: FeedRouter // REQUIRED — resolves relay selections
getPubkeysForScope: (scope: Scope) => string[]
getPubkeysForWOTRange: (min: number, max: number) => string[]
signer?: ISigner
signal?: AbortSignal
context?: AdapterContext // net context: {pool, repository, getAdapter?}
}
FeedController
Orchestrates loading/listening with pagination, deduplication, and set-operation handling.
class FeedController {
compiler: FeedCompiler
constructor(options: FeedControllerOptions)
async load(limit: number): Promise<void>
listen(): () => Promise<void>
async getLoader(): Promise<(limit: number) => Promise<void>>
async getListener(): Promise<() => Promise<void>>
async getRequestItems(): Promise<RequestItem[] | undefined>
}
type FeedControllerOptions = FeedCompilerOptions & {
feed: Feed
tracker?: Tracker
onEvent?: (event: TrustedEvent) => void
onExhausted?: () => void
useWindowing?: boolean
}
Routing (FeedRouter)
@welshman/feeds has no way to turn a pubkey into relay urls, so it declares the capability as an interface and the caller supplies it.
import type {RelaySelection, RelayScenario} from '@welshman/util'
export interface FeedRouter {
resolve(selections: RelaySelection[]): Promise<RelayScenario>
}
// Decide which relays serve which filters under the outbox model
getFilterSelections(filters: Filter[], router: FeedRouter): Promise<RelaysAndFilters[]>
// RelaysAndFilters = {relays: string[]; filters: Filter[]}
getFilterSelections applies one rule per outbox source: a search filter goes to searchRelays(10); a gift-wrap filter with no authors goes to userMessaging(); a filter with authors is chunked and sent to each author's outbox(); and everything additionally gets a low-weight userInbox(0.2) pass. Each group resolves with addMinimalFallbacks.
@welshman/app's Router plugin implements FeedRouter, and app.use(Feeds).makeFeedController(...) supplies it (along with getPubkeysForScope, getPubkeysForWOTRange, the signer, and the app's net context) so you only pass feed and your callbacks. The RelaySelection DSL itself is documented in the welshman-util skill.
Display & validation helpers
display* functions render a feed definition as human-readable text — displayFeed(feed) dispatches on type, with displayAuthorFeed, displayKindFeed, displayScopeFeed, displayTagFeed, … underneath, plus displayFeeds(feeds) for a list.
validate* functions throw on a malformed feed tuple — validateFeed(feed) dispatches, with validateAuthorFeed, validateDVMFeed, validateFeedArgs, validateTagFeedMapping, … underneath. Run validateFeed on anything decoded from a kind-31890 event before compiling it.
Common Patterns
1. Simple author + kind feed
import { FeedController, makeIntersectionFeed, makeAuthorFeed, makeKindFeed } from '@welshman/feeds'
import { Scope } from '@welshman/feeds'
const controller = new FeedController({
router, // a FeedRouter — e.g. app.use(Router)
feed: makeIntersectionFeed(
makeAuthorFeed("pubkey1", "pubkey2"),
makeKindFeed(1),
),
getPubkeysForScope: (scope) => [],
getPubkeysForWOTRange: (min, max) => [],
onEvent: (event) => console.log(event.id),
onExhausted: () => console.log('done'),
})
await controller.load(50)
2. Follows feed with WOT filtering
import {
FeedController, makeIntersectionFeed, makeScopeFeed,
makeWOTFeed, makeKindFeed, Scope
} from '@welshman/feeds'
const controller = new FeedController({
router,
feed: makeIntersectionFeed(
makeScopeFeed(Scope.Follows),
makeWOTFeed({ min: 0.1 }),
makeKindFeed(1, 6, 7),
),
getPubkeysForScope: (scope) => {
if (scope === Scope.Follows) return myFollowList
return []
},
getPubkeysForWOTRange: (min, max) => wotIndex.getPubkeys(min, max),
onEvent: handleEvent,
onExhausted: markExhausted,
useWindowing: true,
})
await controller.load(20)
3. DVM-powered algorithmic feed
import {
FeedController, makeIntersectionFeed, makeDVMFeed,
makeWOTFeed, FeedType
} from '@welshman/feeds'
// DVMItem.mappings controls how DVM result tags become sub-feeds
const controller = new FeedController({
router,
feed: makeIntersectionFeed(
makeDVMFeed({
kind: 5300,
tags: [['p', 'dvm-pubkey-hex']],
mappings: [['p', [FeedType.Author]]],
}),
makeWOTFeed({ min: 0.05 }),
),
getPubkeysForScope: () => [],
getPubkeysForWOTRange: (min, max) => wotPubkeys,
onEvent: handleEvent,
})
await controller.load(30)
4. List-based feed (NIP-51 list)
import { FeedController, makeListFeed, makeKindFeed, makeUnionFeed, FeedType } from '@welshman/feeds'
const controller = new FeedController({
router,
feed: makeUnionFeed(
makeListFeed({
addresses: ["10003:pubkey:identifier"],
// default tag mappings applied unless overridden
}),
makeKindFeed(1),
),
getPubkeysForScope: () => [],
getPubkeysForWOTRange: () => [],
onEvent: handleEvent,
})
await controller.load(25)
5. Converting existing filters to a feed
import { ago, HOUR } from '@welshman/lib'
import { feedFromFilters, FeedCompiler } from '@welshman/feeds'
const filters = [
{ kinds: [1], authors: ["pubkey1"], since: ago(HOUR) },
{ kinds: [6], "#e": ["event-id"] },
]
const feed = feedFromFilters(filters)
const compiler = new FeedCompiler({
router,
getPubkeysForScope: () => [],
getPubkeysForWOTRange: () => [],
})
const requestItems = await compiler.compile(feed)
// => [{filters: [{kinds:[1], authors:["pubkey1"], since:...}]}, ...]
6. Traversing a feed tree to inspect contents
import { walkFeed, isKindFeed, isAuthorFeed, getFeedArgs } from '@welshman/feeds'
const kinds = new Set<number>()
const authors = new Set<string>()
walkFeed(myFeed, (node) => {
if (isKindFeed(node)) getFeedArgs(node).forEach(k => kinds.add(k))
if (isAuthorFeed(node)) getFeedArgs(node).forEach(p => authors.add(p))
})
console.log('Kinds in feed:', [...kinds])
console.log('Authors in feed:', [...authors])
Integration Notes
@welshman/util—Filter,TrustedEvent, and nostr primitives used throughout.getIdFilters()is used internally by the compiler for address feeds.@welshman/signer—ISignerinterface, passed optionally throughFeedCompilerOptionsfor DVM requests that require signing.@welshman/net— TheFeedControllerdelegates torequestPagefor relay communication. TheFeedCompilerdelegates torequestDVMfor DVM-based feeds. Neither acceptsrequestorrequestDVMas constructor options.AdapterContextfrom net is passed throughFeedCompilerOptions.@welshman/app—app.use(Feeds).makeFeedController({feed, onEvent, …})suppliesrouter(theRouterplugin),getPubkeysForScope/getPubkeysForWOTRange(fromWot), the user's signer, and the app's{pool, repository}context.Feedsis also the kind-31890 saved-feed collection.Tracker— Optional deduplication helper (from@welshman/netor app layer). Pass a sharedTrackerinstance to avoid re-emitting events seen in other controllers.
Gotchas & Tips
-
routeris required.FeedCompilerOptions.routerhas no default; aFeedControllerorFeedCompilerconstructed without one will fail when it tries to resolve relays. In an app, go throughapp.use(Feeds).makeFeedController(...). -
Always use factory functions (
makeAuthorFeed, etc.) rather than constructing raw tuples — the tuple structure is internal and type safety depends on using factories. -
useWindowing: trueis for relays that may return events out of chronological order. Do not use it for DVM/algorithmic feeds where order is part of the result. -
FeedController.load()is stateful — each call continues from where the last left off (pagination). Create a new controller to reset. -
canCompilereturnsfalseonly forFeedType.Difference(and recursively forUnion/Intersectionwhose sub-feeds include aDifference). DVM and List feeds returntruefromcanCompileand are compiled asynchronously by_compileDvmsand_compileListsinside the compiler'scompilemethod. The feeds handled specially byFeedController(outside the compiled request flow) areDifference,Union, andIntersection— but only whencanCompilereturnsfalsefor them. -
simplifyFeedflattens nested same-type set operations (e.g.union(union(a,b), c)→union(a,b,c)). Run it before storing or serializing feed definitions. -
makeDifferenceFeed(included, ...excluded)— the first argument is the base feed to include; all subsequent feeds define events to exclude. -
Tag key convention — tag feeds use
#t,#e, etc. (hash-prefixed) to match Nostr filter syntax.makeTagFeed("#t", "bitcoin")produces filter{"#t": ["bitcoin"]}. -
CreatedAtItem.relative— when set to["since"]or["until"], the compiler treats those timestamps as relative offsets from the current time rather than absolute unix timestamps. -
Intersection of feeds is AND logic across relay results — an event must appear in responses from ALL sub-feeds to be emitted. This can significantly reduce result counts vs. a union.