Skip to content
relay-client logo

Relay Client & Subscriptions

relay-client

Subscription and filter-assembly patterns for the Amethyst relay client layer in `commons/.../relayClient/`. Use when working with compose-scoped subscriptions (`ComposeSubscriptionManager`, `Subscribable`), filter assemblers (`MetadataFilterAssembler`, `ReactionsFilterAssembler`, `FeedMetadataCo...

vitorpamplona/amethyst0installs1.6Kstars

SKILL.md

Full skill instructions

Relay Client & Subscriptions

The layer between LocalCache/Account and the raw relay connection. Ensures composables only subscribe to what is visible, deduplicates filters across screens, and rate-limits bulk queries like "fetch metadata for these 200 pubkeys".

When to Use This Skill

  • Adding a new screen that needs events it doesn't already have (write a FilterAssembler).
  • Wiring a composable to subscribe on enter / unsubscribe on leave (ComposeSubscriptionManager).
  • Preloading metadata / profile pictures for a set of pubkeys (MetadataPreloader).
  • Deduplicating identical filters across concurrent screens.
  • Handling EOSE → "we have historical data, stop showing loading" transitions.

Layout

All under commons/​src/​commonMain/​kotlin/​com/​vitorpamplona/​amethyst/​commons/​relayClient/:

relayClient/
├── assemblers/              # "Given these inputs, build this relay Filter"
│   ├── MetadataFilterAssembler.kt      # kind 0 for N pubkeys
│   ├── ReactionsFilterAssembler.kt     # kind 7 for N note ids
│   └── FeedMetadataCoordinator.kt      # coordinates metadata loads for a feed
├── composeSubscriptionManagers/
│   ├── ComposeSubscriptionManager.kt              # interface Subscribable<T>
│   ├── MutableComposeSubscriptionManager.kt       # reference impl
│   └── ComposeSubscriptionManagerControls.kt      # DisposableEffect-style controls
├── eoseManagers/            # EOSE tracking per subscription
├── preload/
│   ├── MetadataPreloader.kt            # bulk-fetch metadata with rate limiting
│   └── MetadataRateLimiter.kt          # token-bucket-ish limiter
└── subscriptions/
    └── KeyDataSourceSubscription.kt    # "this set of keys drives this filter"

Core Concept: Subscribable<T>

// composeSubscriptionManagers/​ComposeSubscriptionManager.kt
interface Subscribable<T> {
    val state: StateFlow<T>
    fun subscribe()
    fun unsubscribe()
}

Every feature-level manager implements or embeds a Subscribable. The MutableComposeSubscriptionManager reference implementation uses reference-counting so that two screens asking for the same feed share one subscription, and only the last leaver actually closes it.

ComposeSubscriptionManagerControls.kt provides DisposableEffect-style helpers so composables don't leak subscriptions when the user navigates away or the process backgrounds.

Typical Flow

@Composable
fun ProfileHeader(pubKey: HexKey) {
    val subscription = rememberSubscribable(pubKey) {
        MetadataFilterAssembler(setOf(pubKey)).toSubscribable()
    }
    LaunchedEffect(pubKey) { subscription.subscribe() }
    DisposableEffect(pubKey) { onDispose { subscription.unsubscribe() } }

    val metadata by subscription.state.collectAsStateWithLifecycle()
    // render metadata…
}

The assembler produces a Filter (see quartz/​.../​nip01Core/​relay/​RelayFilters.kt in the quartz module). The RelayPool below dedups, opens subs, emits events to LocalCache.consume, and emits EOSE through the eose manager.

Assemblers

An assembler is a plain class:

class MetadataFilterAssembler(
    private val pubKeys: Set<HexKey>,
) {
    fun toFilter(): Filter = filter {
        kinds(MetadataEvent.KIND)
        authors(pubKeys)
        limit(pubKeys.size)
    }
}

Assemblers stay pure — no state, no I/​O. They're the composition seam: FeedMetadataCoordinator takes a list of visible notes and assembles a single metadata filter covering every referenced pubkey.

Preloaders

MetadataPreloader is the "I need metadata for 200 pubkeys, but don't melt my CPU or the relay" path. It uses MetadataRateLimiter (token bucket) to throttle bulk fetches and group them into relay-friendly chunks.

Related: amethyst/​.../​service/​images/​ImageLoaderSetup.kt also uses preloaders for blurhash hydration — they're a general pattern, not metadata-specific.

EOSE Handling

Each subscription tracks "End of Stored Events" per relay. The eose manager in eoseManagers/ aggregates per-relay EOSE into a single "loading done" boolean that the UI uses to hide spinners. Without aggregation, composables would flicker as individual relays ack.

Patterns

DO

  • Build one Subscribable per feature scope (screen / dialog / card).
  • Dedupe via reference counting — multiple identical subscriptions should share.
  • Use DisposableEffect / LaunchedEffect to tie sub/​unsub to lifecycle.
  • Put the relay Filter building in an assembler so the test is trivial.
  • Route bulk metadata through MetadataPreloader; don't fire N subscriptions.

DON'T

  • Don't call RelayPool / NostrClient directly from composables — always through a Subscribable.
  • Don't hold a subscription past the composable's lifetime — memory & socket leaks.
  • Don't build ad-hoc filters inline in composables — assemblers only.
  • Don't preload metadata for everything — it's a rate-limited resource and competes with user-visible loads.

Related

  • nostr-expert/​references/​tag-patterns.md — how tags inform what a filter needs to look for.
  • kotlin-coroutines/​references/​relay-patterns.md — relay pool internals (sibling layer beneath assemblers).
  • feed-patterns skill — feeds compose several Subscribables (content + metadata + reactions).
  • account-state skill — Account's per-kind flows are themselves consumers of the relay-client layer.

More skills from vitorpamplona

feed-patterns logo
vitorpamplona/amethyst

feed-patterns

Feed composition and data-access layer patterns in Amethyst. Use when adding or modifying a feed (home, profile, hashtag, bookmarks, notifications, DMs, communities), working with `FeedFilter` / `AdditiveComplexFeedFilter` / `ChangesFlowFilter` / `FilterByListParams` in `amethyst/.../ui/dal/`, or...

1.6K 0
View
quartz-integration logo
vitorpamplona/amethyst

quartz-integration

Integration guide for using the Quartz Nostr KMP library in external projects. Use when: (1) adding Quartz as a Gradle dependency, (2) setting up NostrClient with WebSocket, (3) creating/signing/sending events, (4) building relay subscriptions with Filter, (5) handling keys with KeyPair/NostrSign...

1.6K 0
View
amy-expert logo
vitorpamplona/amethyst

amy-expert

Patterns for extending `amy`, the Amethyst CLI in `cli/`. Use when adding an `amy <verb>` command, touching files under `cli/src/main/kotlin/…/cli/`, wiring a new subcommand into `Main.kt`, writing an interop test script that drives Amy, or extracting logic out of `amethyst/` into `commons/` so a...

1.6K 0
View
auth-signers logo
vitorpamplona/amethyst

auth-signers

Signer abstraction patterns in Amethyst. Use when working with event signing, choosing between a local keypair (`NostrSignerInternal`), a remote NIP-46 bunker signer (`NostrSignerRemote`), or a NIP-55 Android external-app signer (`NostrSignerExternal`). Covers the abstract `NostrSigner` base clas...

1.6K 0
View
gradle-expert logo
vitorpamplona/amethyst

gradle-expert

Build optimization, dependency resolution, and multi-module KMP troubleshooting for AmethystMultiplatform. Use when working with: (1) Gradle build files (build.gradle.kts, settings.gradle), (2) Version catalog (libs.versions.toml), (3) Build errors and dependency conflicts, (4) Module dependencie...

1.6K 0
View
account-state logo
vitorpamplona/amethyst

account-state

Account state and in-memory event store patterns in Amethyst. Use when working with `Account.kt` (per-user StateFlow properties — follow list, relays, settings, mutes, bookmarks), `LocalCache` (the object-level event store backed by `LargeCache`), `User`/`Note` model classes, or any ViewModel tha...

1.6K 0
View
android-expert logo
vitorpamplona/amethyst

android-expert

Android platform patterns for the `amethyst/` module. Use when working with (1) Android navigation (Navigation Compose, type-safe routes, bottom nav), (2) runtime permissions (camera, notifications, biometrics), (3) platform APIs (Intent, Context, Activity, ContentResolver), (4) Material3 theming...

1.6K 0
View
Amethyst logo
vitorpamplona/amethyst

Amethyst

Build customized Amethyst Nostr clients for Android. Fork, rebrand, customize, and distribute your own version.

1.6K 0
View
kotlin-coroutines logo
vitorpamplona/amethyst

kotlin-coroutines

Advanced Kotlin coroutines patterns for AmethystMultiplatform. Use when working with: (1) Structured concurrency (supervisorScope, coroutineScope), (2) Advanced Flow operators (flatMapLatest, combine, merge, shareIn, stateIn), (3) Channels and callbackFlow, (4) Dispatcher management and context s...

1.6K 0
View
kotlin-expert logo
vitorpamplona/amethyst

kotlin-expert

Advanced Kotlin patterns for AmethystMultiplatform. Flow state management (StateFlow/SharedFlow), sealed hierarchies (classes vs interfaces), immutability (@Immutable, data classes), DSL builders (type-safe fluent APIs), inline functions (reified generics, performance). Use when working with: (1)...

1.6K 0
View
find-missing-translations logo
vitorpamplona/amethyst

find-missing-translations

Use when comparing Android strings.xml locale files to find untranslated string resources, missing translation keys, or preparing translation work for a specific language

1.6K 0
View
find-non-lambda-logs logo
vitorpamplona/amethyst

find-non-lambda-logs

Use when auditing or migrating Log calls — flags both interpolated Log.d/i/w/e that should use the lambda overload (allocation hygiene) and catch-block Log.w/e that interpolate ${e.message} but drop the throwable (lost stack traces)

1.6K 0
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View