feat(relay): NIP-65 inbox routing for tagged-user events (R9)#360
feat(relay): NIP-65 inbox routing for tagged-user events (R9)#360spe1020 merged 2 commits intozapcooking:mainfrom
Conversation
Per NIP-65, when an event tags users via p tags (replies, mentions,
reactions, comments), clients SHOULD include those users' read
relays in the publish target so the events actually land where the
recipients read them. Today none of the publish paths consult
relayListCache for this, even though the cache is fully wired for
read flows.
Adds src/lib/nip65Routing.ts as the single source of truth:
- getRecipientInboxRelays(event): pulls non-self 64-hex pubkeys from
the event's p tags, looks them up via the existing relayListCache
(SWR + IndexedDB), bounded by INBOX_LOOKUP_TIMEOUT_MS (1.5s) so a
hung discovery relay can't stall the publish path.
- unionInboxRelayUrls(event, baseUrls): unions caller-supplied base
URLs with inbox URLs, dedupes, caps at MAX_PUBLISH_RELAYS (16) so
a heavily p-tagged post can't fan out unbounded.
- buildInboxAwareRelaySet(opts): convenience for direct publishers
that want an NDKRelaySet — falls back to the NDK pool's relays
when no base set is supplied (matches the prior event.publish()
no-args default), with inbox URLs added on top.
Wired into three publish paths:
1. publishQueue.attemptPublish — refactors the four-mode branched
logic into a unified URL-collection step. relayMode (garden /
pantry / garden-pantry / all) still picks the base relays
exactly as today; inbox URLs are layered on. User intent takes
precedence: when the cap kicks in, base relays survive and inbox
URLs may be dropped.
2. publishReaction (kind 7): replaces reactionEvent.publish() (NDK
default) with a publish to the inbox-aware set. Reaction now
reaches the reacted-to author's read relays.
3. comments/postComment (kind 1 / 1111): both signing strategies
('explicit-with-timeout' and 'implicit') now publish to an inbox-
aware set built from the event's full p-tag set (parent author
+ thread participants + @-mentions).
Behavior is purely additive — events with no p tags fall through to
existing relay strategy unchanged. Cache misses / lookup timeouts
fall back to base relays only, never block the publish.
Closes R9 from docs/perf/2026-04-review.md (zapcooking#341).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds NIP-65 “inbox relay” routing on the publish path so recipient-targeted events (those with p tags) are also published to the recipients’ kind:10002 read relays, improving deliverability for replies/mentions/reactions/comments.
Changes:
- Introduces shared NIP-65 routing helpers (
getRecipientInboxRelays,unionInboxRelayUrls,buildInboxAwareRelaySet) backed byrelayListCacheand capped fanout. - Refactors
publishQueue.attemptPublishto compute a single target relay URL list perrelayMode, then layer inbox routing on top. - Updates reaction and comment publishers to publish via an inbox-aware
NDKRelaySet(with graceful fallback).
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/lib/nip65Routing.ts |
New shared helper module for inbox relay discovery/union/capping + relay-set building. |
src/lib/publishQueue.ts |
Collects base relay URLs by mode, unions inbox relays, then publishes to a materialized NDKRelaySet. |
src/lib/reactions/publishReaction.ts |
Publishes kind:7 reactions to an inbox-aware relay set. |
src/lib/comments/postComment.ts |
Publishes comments (explicit + implicit signing paths) to an inbox-aware relay set. |
package.json |
Bumps app version. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export async function getRecipientInboxRelays(event: NDKEvent): Promise<string[]> { | ||
| const targets = new Set<string>(); | ||
| for (const tag of event.tags) { | ||
| if (tag[0] !== 'p' || !tag[1] || !HEX64_RE.test(tag[1])) continue; | ||
| // Skip self — no need to publish to our own inbox. | ||
| if (event.pubkey && tag[1] === event.pubkey) continue; | ||
| targets.add(tag[1]); | ||
| } |
There was a problem hiding this comment.
This file is using tab indentation (e.g., in getRecipientInboxRelays/unionInboxRelayUrls/buildInboxAwareRelaySet), but the repo’s Prettier config sets "useTabs": false. Please run Prettier (or otherwise reformat) so indentation uses spaces consistently with the project formatting settings.
| const inboxUrls = await getRecipientInboxRelays(event); | ||
| const seen = new Set<string>(); | ||
| const out: string[] = []; | ||
| for (const url of [...baseUrls, ...inboxUrls]) { | ||
| if (out.length >= MAX_PUBLISH_RELAYS) break; | ||
| if (seen.has(url)) continue; | ||
| seen.add(url); | ||
| out.push(url); | ||
| } |
There was a problem hiding this comment.
unionInboxRelayUrls() dedupes raw URL strings, but elsewhere in the codebase relay URLs can appear with/without trailing slashes (e.g. pantry comparisons handle both forms). Without normalizing before dedupe/capping, the same relay may consume multiple slots and cause the 16-relay cap to drop distinct relays. Consider normalizing (trim/strip trailing slashes + consistent casing/protocol) for the dedupe key while preserving the first-seen original URL for connection.
| private getBaseRelayUrls(relayMode: PendingPublish['relayMode'], ndkInstance: any): string[] { | ||
| if (relayMode === 'garden') return ['wss://garden.zap.cooking']; | ||
| if (relayMode === 'pantry') return ['wss://pantry.zap.cooking']; | ||
| if (relayMode === 'garden-pantry') { | ||
| return ['wss://garden.zap.cooking', 'wss://pantry.zap.cooking']; | ||
| } |
There was a problem hiding this comment.
getBaseRelayUrls() hardcodes garden/pantry relay URLs even though the codebase already defines canonical relay URLs/constants (e.g. GARDEN_RELAY_URL in $lib/nostr, and relay sets in $lib/relays/relaySets). To avoid drift and subtle dedupe issues, prefer reusing the shared constants/relay-set definitions here instead of repeating literal strings.
- Reformat src/lib/nip65Routing.ts with prettier (project's useTabs=false). The new file landed with tab indentation on first pass; this just runs prettier so it matches the rest of the repo. - Normalize relay URLs before dedupe in unionInboxRelayUrls. Without normalization, 'wss://example.com' and 'wss://example.com/' would consume two slots in the 16-relay cap and could drop distinct relays. Reuses normalizeRelayUrl already exported from relayListCache. Original spelling is preserved in the result so NDK's pool keys (raw URLs) keep their existing connection state. - Replace hardcoded garden / pantry literals in publishQueue.getBaseRelayUrls() with shared constants. Adds PANTRY_RELAY_URL to $lib/nostr next to the existing GARDEN_RELAY_URL so both have a single source of truth and changes propagate everywhere instead of drifting between literals. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Thanks @copilot — addressed in c3184be:
|
Summary
Closes R9 from `docs/perf/2026-04-review.md` (#341). Today, when a user replies to / mentions / reacts on / comments on someone's note, those events don't reach the recipient's NIP-65 inbox relays (kind:10002 read markers). The cache layer (`relayListCache.ts`) was already fully wired for read flows; nothing on the publish path consulted it.
This PR adds inbox routing to all three publish paths that fan out to specific recipients.
Approach
New shared module `src/lib/nip65Routing.ts`:
Wired into
Behavior is purely additive
Files changed
Test plan
🤖 Generated with Claude Code