Skip to content

feat(relay): NIP-65 inbox routing for tagged-user events (R9)#360

Merged
spe1020 merged 2 commits intozapcooking:mainfrom
spe1020:feat/publish-queue-nip65-routing
Apr 26, 2026
Merged

feat(relay): NIP-65 inbox routing for tagged-user events (R9)#360
spe1020 merged 2 commits intozapcooking:mainfrom
spe1020:feat/publish-queue-nip65-routing

Conversation

@spe1020
Copy link
Copy Markdown
Contributor

@spe1020 spe1020 commented Apr 26, 2026

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

  • `getRecipientInboxRelays(event)` — extracts non-self 64-hex pubkeys from `p` tags, looks them up via `relayListCache` (SWR + IndexedDB; typical lookup is a memory hit). Bounded by 1.5s so a hung discovery relay can't stall publishing.
  • `unionInboxRelayUrls(event, baseUrls)` — unions caller-supplied base URLs with inbox URLs, dedupes, caps at 16 relays total so a heavily p-tagged post stays bounded. Base URLs are kept first so the user's chosen `relayMode` survives the cap.
  • `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

  1. `publishQueue.attemptPublish` — refactors the four-mode branched logic (`garden` / `pantry` / `garden-pantry` / `all`) into a unified URL-collection step. Mode still picks base relays exactly as today; inbox URLs are layered on. PostComposer (replies, mentions) and PollDisplay (votes) get the fix automatically.
  2. `publishReaction.ts` (kind 7) — replaces `reactionEvent.publish()` with a publish to the inbox-aware set. Reactions now reach the reacted-to author's read relays.
  3. `comments/postComment.ts` (kind 1 / 1111) — both signing strategies 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 — inbox lookup never blocks the publish.
  • `relayMode` selection still takes precedence; inbox URLs are augmentation, not replacement.

Files changed

  • new `src/lib/nip65Routing.ts` — shared helper (~150 lines)
  • `src/lib/publishQueue.ts` — refactor 4-mode branches into unified flow + inbox augmentation
  • `src/lib/reactions/publishReaction.ts` — publish to inbox-aware set
  • `src/lib/comments/postComment.ts` — publish to inbox-aware set in both signing strategies

Test plan

  • Reply on a post from a user whose kind:10002 lists relays we don't connect to → reply lands on their inbox.
  • React with an emoji → kind 7 lands on author's inbox.
  • Comment (NIP-22 kind 1111) on a recipe → comment lands on author's inbox.
  • Publishing a recipe (kind 30023, no `p` tags) — relay set unchanged from prior behavior.
  • User without a kind:10002 — falls through to base relay set, no errors.
  • Heavily p-tagged thread (e.g. 8 participants) — total relay count stays at or below 16.
  • Discovery relay is slow / down — publish still succeeds within a few seconds.

🤖 Generated with Claude Code

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>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 by relayListCache and capped fanout.
  • Refactors publishQueue.attemptPublish to compute a single target relay URL list per relayMode, 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.

Comment thread src/lib/nip65Routing.ts Outdated
Comment on lines +49 to +56
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]);
}
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/lib/nip65Routing.ts Outdated
Comment on lines +90 to +98
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);
}
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/lib/publishQueue.ts Outdated
Comment on lines +414 to +419
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'];
}
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
- 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>
@spe1020
Copy link
Copy Markdown
Contributor Author

spe1020 commented Apr 26, 2026

Thanks @copilot — addressed in c3184be:

  • Prettier formatting — reran prettier on `src/lib/nip65Routing.ts`; tabs → 2-space indent to match `useTabs: false`.
  • URL normalization in dedupe — `unionInboxRelayUrls` now keys the dedupe set on `normalizeRelayUrl(url)` (already exported from `relayListCache`), so `wss://example.com` and `wss://example.com/` collapse to one slot. Original spelling is preserved in the result — NDK's relay pool keys by raw URL, so handing it the variant we've already seen elsewhere keeps existing connection state intact.
  • Hardcoded relay URLs — replaced literals in `publishQueue.getBaseRelayUrls()` with shared constants. Added `PANTRY_RELAY_URL` to `$lib/nostr` next to the existing `GARDEN_RELAY_URL` for symmetry. Single source of truth for both.

@spe1020 spe1020 merged commit 05a8418 into zapcooking:main Apr 26, 2026
1 check passed
@spe1020 spe1020 deleted the feat/publish-queue-nip65-routing branch April 26, 2026 23:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants