mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-12 01:07:46 +00:00
feat(cli): cache-first reads across feed, dm, marmot commands
Audit found four commands draining replaceable relay-list events
(kind:10050, 10051, 10002) on every invocation, plus one fetching
the user's own kind:3 contact list. All five are now cache-first.
New Context helpers:
- dmInboxOf(pk) → kind:10050 (ChatMessageRelayListEvent)
- keyPackageRelaysOf(pk)→ kind:10051 (KeyPackageRelayListEvent)
- cachedRelayListsOf(pk)→ assembles a RecipientRelayFetcher.Lists
from the local store; returns null if
nothing is cached so callers can fall
back with `?:`.
Wired into:
- FeedCommand.resolveFollowing — replaces a kind:3 drain with
ctx.contactsOf(self).
- DmCommands.resolveDmRelays — `amy dm send` / `dm list`.
- KeyPackageCommands.check.
- AwaitCommands (key-package / member / admin polling loops).
- GroupAddMemberCommand (one cache hit per invitee).
All five sites now go: try cache → if miss, run the existing
RecipientRelayFetcher / drain. The drain itself populates the cache
via verifyAndStore, so subsequent runs are local-only. Replaceable
slot shortcut means each cached read is one stat + one readString,
not a directory walk.
README "Local event store" section gains a list of which commands
are cache-first today.
This commit is contained in:
+17
-3
@@ -70,11 +70,25 @@ from the store first and only fall back to a relay fetch on miss.
|
||||
Three convenience helpers exist on `Context`:
|
||||
|
||||
```kotlin
|
||||
ctx.profileOf(pubKey) // latest kind:0 (NIP-01)
|
||||
ctx.relaysOf(pubKey) // latest kind:10002 (NIP-65)
|
||||
ctx.contactsOf(pubKey) // latest kind:3 (NIP-02)
|
||||
ctx.profileOf(pubKey) // latest kind:0 (NIP-01)
|
||||
ctx.relaysOf(pubKey) // latest kind:10002 (NIP-65)
|
||||
ctx.contactsOf(pubKey) // latest kind:3 (NIP-02)
|
||||
ctx.dmInboxOf(pubKey) // latest kind:10050 (NIP-17 DM inbox)
|
||||
ctx.keyPackageRelaysOf(pubKey) // latest kind:10051 (MIP-00 KP relays)
|
||||
ctx.cachedRelayListsOf(pubKey) // RecipientRelayFetcher.Lists from cache
|
||||
```
|
||||
|
||||
Commands that already read these cache-first:
|
||||
|
||||
- `amy profile show` — `--refresh` to bypass.
|
||||
- `amy feed --following` — local kind:3 served via slot lookup; falls
|
||||
back to a relay drain on first run.
|
||||
- `amy dm send` — recipient's kind:10050 / 10051 / 10002 served from
|
||||
cache before falling back to `RecipientRelayFetcher`.
|
||||
- `amy marmot key-package check` and `amy marmot await key-package`
|
||||
— same recipient-relay lookup, cache-first.
|
||||
- `amy marmot group add` — invitee relay lists served from cache.
|
||||
|
||||
The store implements every feature of the Quartz SQLite store —
|
||||
NIP-01 replaceable / addressable uniqueness, NIP-09 deletion
|
||||
tombstones, NIP-40 expiration, NIP-50 search, NIP-62 right-to-vanish,
|
||||
|
||||
@@ -28,6 +28,8 @@ import com.vitorpamplona.amethyst.commons.defaults.DefaultNIP65RelaySet
|
||||
import com.vitorpamplona.amethyst.commons.marmot.MarmotManager
|
||||
import com.vitorpamplona.amethyst.commons.marmot.ingest
|
||||
import com.vitorpamplona.quartz.marmot.MarmotFilters
|
||||
import com.vitorpamplona.quartz.marmot.RecipientRelayFetcher
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent
|
||||
import com.vitorpamplona.quartz.marmot.mip03GroupMessages.GroupEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
@@ -44,6 +46,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip01Core.store.IEventStore
|
||||
import com.vitorpamplona.quartz.nip01Core.store.fs.FsEventStore
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent
|
||||
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
@@ -333,6 +336,54 @@ class Context(
|
||||
Filter(authors = listOf(pubKey), kinds = listOf(ContactListEvent.KIND), limit = 1),
|
||||
).firstOrNull() as? ContactListEvent
|
||||
|
||||
/**
|
||||
* Latest known kind:10050 chat-message (NIP-17 DM) inbox relay list
|
||||
* for [pubKey], or `null` if Amy has never observed one. Used by
|
||||
* `dm send` to resolve where to deliver a wrap.
|
||||
*/
|
||||
fun dmInboxOf(pubKey: HexKey): ChatMessageRelayListEvent? =
|
||||
store
|
||||
.query<Event>(
|
||||
Filter(authors = listOf(pubKey), kinds = listOf(ChatMessageRelayListEvent.KIND), limit = 1),
|
||||
).firstOrNull() as? ChatMessageRelayListEvent
|
||||
|
||||
/**
|
||||
* Latest known kind:10051 KeyPackage relay list (MIP-00) for
|
||||
* [pubKey], or `null` if Amy has never observed one. Used by
|
||||
* `marmot key-package check` and `marmot await key-package` to
|
||||
* locate where the recipient publishes their KeyPackages.
|
||||
*/
|
||||
fun keyPackageRelaysOf(pubKey: HexKey): KeyPackageRelayListEvent? =
|
||||
store
|
||||
.query<Event>(
|
||||
Filter(authors = listOf(pubKey), kinds = listOf(KeyPackageRelayListEvent.KIND), limit = 1),
|
||||
).firstOrNull() as? KeyPackageRelayListEvent
|
||||
|
||||
/**
|
||||
* Assemble a [RecipientRelayFetcher.Lists] from the local store —
|
||||
* the same shape callers get from [RecipientRelayFetcher.fetchRelayLists]
|
||||
* after a network drain, but no network round-trip. Returns `null`
|
||||
* only when the cache has *no* relay-list events at all for
|
||||
* [pubKey] (none of kind 10050 / 10051 / 10002), so callers can
|
||||
* trivially fall back to the network fetcher with `?:`.
|
||||
*
|
||||
* Stale-data caveat: replaceable events are immutable per snapshot
|
||||
* — if the recipient has rotated their inbox since we last saw them,
|
||||
* we'll still hand back the old list. Commands that care can drain
|
||||
* (which re-populates the cache) or expose a `--refresh` flag.
|
||||
*/
|
||||
fun cachedRelayListsOf(pubKey: HexKey): RecipientRelayFetcher.Lists? {
|
||||
val dm = dmInboxOf(pubKey)
|
||||
val kp = keyPackageRelaysOf(pubKey)
|
||||
val nip65 = relaysOf(pubKey)
|
||||
if (dm == null && kp == null && nip65 == null) return null
|
||||
return RecipientRelayFetcher.Lists(
|
||||
dmInbox = dm?.relays().orEmpty(),
|
||||
keyPackage = kp?.relays().orEmpty(),
|
||||
nip65 = nip65,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull down everything needed to bring local Marmot state current:
|
||||
* - kind:1059 gift wraps on inbox relays → try to unwrap Welcomes
|
||||
|
||||
@@ -76,7 +76,13 @@ object AwaitCommands {
|
||||
// target hasn't published either list yet, fall back to the
|
||||
// bootstrap pool so the loop still has something to query.
|
||||
val seed = ctx.bootstrapRelays()
|
||||
val lists = RecipientRelayFetcher.fetchRelayLists(ctx.client, target, seed)
|
||||
// Cache-first: relay lists are kind:10050 / 10051 / 10002 —
|
||||
// all replaceable, served from the local store via the slot
|
||||
// shortcut. Falls back to a network drain only if Amy has
|
||||
// never observed any of them for `target`.
|
||||
val lists =
|
||||
ctx.cachedRelayListsOf(target)
|
||||
?: RecipientRelayFetcher.fetchRelayLists(ctx.client, target, seed)
|
||||
val relays =
|
||||
KeyPackageFetcher.fetchRelaysFor(
|
||||
targetKeyPackageRelays = lists.keyPackage,
|
||||
|
||||
@@ -415,7 +415,13 @@ object DmCommands {
|
||||
allowFallback: Boolean,
|
||||
): RelaySet {
|
||||
val seed = ctx.bootstrapRelays()
|
||||
val lists = RecipientRelayFetcher.fetchRelayLists(ctx.client, recipient, seed)
|
||||
// Cache-first: if Amy has previously seen the recipient's
|
||||
// kind:10050 / 10051 / 10002 events, use the local copy and
|
||||
// skip the network drain entirely. Falls back to the live
|
||||
// fetcher only if the local store has nothing.
|
||||
val lists =
|
||||
ctx.cachedRelayListsOf(recipient)
|
||||
?: RecipientRelayFetcher.fetchRelayLists(ctx.client, recipient, seed)
|
||||
val dmInbox = lists.dmInbox.toSet()
|
||||
if (dmInbox.isNotEmpty()) return RelaySet(dmInbox, "kind_10050")
|
||||
if (!allowFallback) return RelaySet(emptySet(), "kind_10050")
|
||||
|
||||
@@ -155,6 +155,13 @@ object FeedCommand {
|
||||
ctx: Context,
|
||||
timeoutMs: Long,
|
||||
): List<HexKey> {
|
||||
// Cache-first: contact lists are kind:3 (replaceable). If we've
|
||||
// ever seen ours, the local store has it already and reading is
|
||||
// a slot lookup — no relay round-trip.
|
||||
ctx.contactsOf(ctx.identity.pubKeyHex)?.let {
|
||||
return it.verifiedFollowKeySet().toList()
|
||||
}
|
||||
|
||||
val relays = ctx.outboxRelays().ifEmpty { ctx.bootstrapRelays() }
|
||||
if (relays.isEmpty()) return emptyList()
|
||||
val filter =
|
||||
|
||||
+10
-5
@@ -72,12 +72,17 @@ object GroupAddMemberCommand {
|
||||
// this the inviter can only broadcast to their own relays,
|
||||
// which silently fails the moment the two users have
|
||||
// disjoint relay configs.
|
||||
//
|
||||
// Cache-first via Context.cachedRelayListsOf — every
|
||||
// relay list seen previously is in the local store
|
||||
// already.
|
||||
val recipient =
|
||||
RecipientRelayFetcher.fetchRelayLists(
|
||||
client = ctx.client,
|
||||
pubKey = pub,
|
||||
seedRelays = seed,
|
||||
)
|
||||
ctx.cachedRelayListsOf(pub)
|
||||
?: RecipientRelayFetcher.fetchRelayLists(
|
||||
client = ctx.client,
|
||||
pubKey = pub,
|
||||
seedRelays = seed,
|
||||
)
|
||||
|
||||
// KeyPackage discovery (MIP-00): prefer the invitee's own
|
||||
// kind:10051, then their kind:10002 write marker, then our
|
||||
|
||||
@@ -77,7 +77,11 @@ object KeyPackageCommands {
|
||||
// when the target and inviter share no relays.
|
||||
val seed = ctx.bootstrapRelays()
|
||||
if (seed.isEmpty()) return Json.error("no_relays", "configure relays first")
|
||||
val recipient = RecipientRelayFetcher.fetchRelayLists(ctx.client, targetHex, seed)
|
||||
// Cache-first via Context.cachedRelayListsOf — replaceable
|
||||
// events live in the local store after the first sync.
|
||||
val recipient =
|
||||
ctx.cachedRelayListsOf(targetHex)
|
||||
?: RecipientRelayFetcher.fetchRelayLists(ctx.client, targetHex, seed)
|
||||
val relays =
|
||||
KeyPackageFetcher.fetchRelaysFor(
|
||||
targetKeyPackageRelays = recipient.keyPackage,
|
||||
|
||||
Reference in New Issue
Block a user