Skip to content
Development
Skill

/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`,

From plugin
amethyst
1.6k30 skills3 commands
Install
$ npx -y skills add vitorpamplona/amethyst --skill relay-client --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/relay-client

Context preview

The summary Claude sees to decide when to auto-load this skill.

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`,

SKILL.md

relay-client.SKILL.md
name: relay-client
description: 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`, `FeedMetadataCoordinator`), preloaders (`MetadataPreloader`, `MetadataRateLimiter`), EOSE managers, or any feature that needs to talk to relays lifecycle-aware from a composable. Complements `nostr-expert` (protocol filter syntax) and `kotlin-coroutines` (callbackFlow patterns).

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/` (the `@Composable` entry points — `observeUser*`, `*FilterAssemblerSubscription`, `KeyDataSourceSubscription` — sit in the same package but in `commonsUI/src/commonMain/…`, the Compose half of the shared layer):

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
│   └── CashuMintDirectoryFilterAssembler.kt / CashuWalletFilterAssembler.kt
├── composeSubscriptionManagers/
│   ├── ComposeSubscriptionManager.kt              # interface Subscribable<T>
│   ├── MutableComposeSubscriptionManager.kt       # reference impl
│   └── ComposeSubscriptionManagerControls.kt      # DisposableEffect-style controls
├── eoseManagers/            # EOSE tracking per subscription
│   └── IEoseManager / BaseEoseManager / PerKeyEoseManager / SingleSubEoseManager
├── nip17Dm/                 # gift-wrap DM plumbing
│   └── FilterGiftWrapsToPubkey.kt / GiftWrapDecryptor.kt
├── 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"
    ├── LifecycleAwareKeyDataSourceSubscription.kt
    └── PrioritizedSubscriptionQueue.kt / SubscriptionPriority.kt

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.

Per-visible loading — the canonical entry points (`observeUser*` / `observeNote*`)

Prefer these over hand-rolled "load metadata for this list" calls. They are the shared, KMP way to load data **only for what's on screen** — a composable subscribes while it is in composition and unsubscribes ~30s after it leaves (or the app backgrounds). Both live in `commons/relayClient/`:

  • **Per user** (`relayClient/user/`): `observeUserInfo/Picture/Banner/AboutMe/Name(user)`

each open a composition-scoped `UserFinderFilterAssemblerSubscription(user)` **and** return reactive `State`. Metadata (kind 0 + relay lists) loads for on-screen users only, coalesced into one batched REQ per relay for the whole visible set.

  • **Per note** (`relayClient/event/`): `EventFinderFilterAssemblerSubscription(note)` loads a

note's interactions (reactions / zaps / reposts / replies) while it is composed. Android's `observeNote*` display observers layer on top of the same subscription.

Both read front-end-provided CompositionLocals — `LocalUserFinder` / `LocalUserFinderAccount` (reused by the event finder) / `LocalEventFinder` — provided once near the composition root (Android `AppModules`, Desktop `Main.kt` via its subscriptions coordinator). The account seam is the narrow `UserFinderAccount` (s

Read more
Ships withamethyst

Nostr client for Android

Get the whole plugin
Stats
1,600
Stars
221
Forks
Active
Maintenance
Kotlin
Language
MIT
License
41m ago
Last commit
3y ago
Created

Repo: vitorpamplona/amethyst

Other skills on amethyst.