Commit 2f5f21e
feat: Route DM gift wraps to recipient's inbox relays (NIP-17 kind 10050) (#44)
* feat: Route DM gift wraps to recipient's inbox relays (NIP-17 kind 10050)
Add inbox_relays module that fetches, caches, and publishes kind 10050
relay lists so DM gift wraps are delivered to each recipient's preferred
inbox relays instead of broadcasting to all pool relays.
- Fetch recipient's 10050 relay list with 1h cache (60s on error)
- Route gift_wrap_to() inbox relays, fall back to pool on failure
- Publish own 10050 on connect advertising read-capable relays
- Republish 10050 (debounced) on every relay config change
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: Add unit tests for inbox_relays tag parsing, cache, and debounce
9 tests covering:
- Tag parsing: extracts URLs, ignores non-relay tags, handles empty/missing
- Cache: store/retrieve, TTL expiry, empty results, error short TTL
- Debounce: generation counter supersedes earlier calls
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: Prevent cache stampede in inbox relay fetches with double-checked locking
When multiple messages are sent rapidly to the same recipient with a cold
cache, each would spawn duplicate fetches for the same kind 10050 event.
This wasted bandwidth and put unnecessary pressure on relays.
Implementation:
- Extracted get_or_fetch_with_lock: generic function with injectable fetch
- Production get_or_fetch_inbox_relays wraps it with fetch_inbox_relays
- Tests use same function with mock fetch, eliminating code duplication
- Double-checked locking: fast path (cache hit) → per-key lock → double-check
- Only first concurrent caller fetches; others wait ~5s then hit cache
- Different pubkeys never block each other (per-key async Mutex)
Memory management (strictly bounded, even on cancellation/panic):
- FETCH_LOCKS stores Weak<Mutex> references, not Arc
- Mutex allocation freed when Arc refcount drops (immediate)
- FetchLockEntryCleanup drop guard ensures cleanup on normal return,
task cancellation, and panic unwind
- Drop implementation checks Arc::strong_count == 2 (guard + upgrade temp)
to detect last holder, then removes map entry
- Periodic retain() every 100 misses as fallback safety net
- True bounded growth: map size = in-flight fetches only, no idle leak
Performance:
- Periodic pruning: O(n/100) amortized cost vs O(n) per miss
- Drop-guard cleanup: O(1) per call, runs unconditionally
- Avoids global critical section bottleneck under heavy miss fan-out
Testing (production code path, zero duplication):
- concurrent_fetches_for_same_pubkey_serialize: 10 tasks → 1 fetch,
verifies lock entry removed after all waiters complete
- fetch_locks_do_not_accumulate_after_calls_complete: verifies drop-guard
removes entries immediately after single-caller fetches
- cancelled_fetch_cleans_up_lock_entry: spawns task with long sleep,
aborts it, verifies entry still removed via drop guard
- TEST_GLOBALS_LOCK serializes ALL tests touching global statics
- All 12 unit tests pass with --test-threads=16
Addresses PR feedback from reviewer testing between Vector and 0xChat.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: alltheseas <alltheseas@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: JSKitty <mail@jskitty.cat>1 parent 925f0e8 commit 2f5f21e
6 files changed
Lines changed: 740 additions & 13 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
410 | 410 | | |
411 | 411 | | |
412 | 412 | | |
| 413 | + | |
413 | 414 | | |
414 | 415 | | |
415 | 416 | | |
| |||
452 | 453 | | |
453 | 454 | | |
454 | 455 | | |
| 456 | + | |
455 | 457 | | |
456 | 458 | | |
457 | 459 | | |
| |||
483 | 485 | | |
484 | 486 | | |
485 | 487 | | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
486 | 491 | | |
487 | 492 | | |
488 | 493 | | |
| |||
525 | 530 | | |
526 | 531 | | |
527 | 532 | | |
| 533 | + | |
528 | 534 | | |
529 | 535 | | |
530 | 536 | | |
| |||
568 | 574 | | |
569 | 575 | | |
570 | 576 | | |
| 577 | + | |
| 578 | + | |
571 | 579 | | |
572 | 580 | | |
573 | 581 | | |
| |||
830 | 838 | | |
831 | 839 | | |
832 | 840 | | |
| 841 | + | |
| 842 | + | |
| 843 | + | |
| 844 | + | |
| 845 | + | |
| 846 | + | |
| 847 | + | |
| 848 | + | |
| 849 | + | |
| 850 | + | |
| 851 | + | |
| 852 | + | |
| 853 | + | |
| 854 | + | |
833 | 855 | | |
834 | 856 | | |
835 | 857 | | |
| |||
0 commit comments