feat: add reply:, ref:, link:, dtag: query keywords#202
Conversation
Supports reply:<hex>, reply:<note1...>, reply:<nevent1...> to find all events referencing a specific event via #e tags. Multiple reply: tokens and optional search terms supported. Part of #201
Supports ref:<kind:pubkey:d-tag> and ref:<naddr1...> to find events referencing a specific replaceable event. Multiple ref: tokens and optional search terms supported. Part of #201
Supports link:<url> to find events referencing a URL via #r tags. Multiple link: tokens and optional search terms supported. Part of #201
- Register new strategies in searchOrchestrator.ts - Add reply:, ref:, link: to STRUCTURED_TOKEN_PATTERN in contentFilter - Add tag filter examples to examples.ts Part of #201
Supports dtag:<identifier> to find replaceable events by d-tag. Multiple dtag: tokens and optional search terms supported. Part of #201
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds structured query token support (id:, reply:, ref:, link:, d:), five new search strategy modules and a shared fetch/dedupe utility, updates orchestrator to prioritize id then run new strategies in sequence, expands token-stripping, appends two search examples, and updates tsconfig excludes. (33 words) Changes
Sequence DiagramsequenceDiagram
participant Client as Search Query
participant Orchestrator as runSearchStrategies
participant Strategy as tryHandle*Search
participant Relays as NDK / Relay Sets
participant Results as fetchDedupeAndSort
Client->>Orchestrator: cleanedQuery + context
Orchestrator->>Strategy: tryHandleIdSearch(cleanedQuery, context)
alt id result found
Strategy-->>Orchestrator: NDKEvent[] (return)
else id not found
Orchestrator->>Strategy: tryHandleReplySearch(...)
alt reply matched
Strategy->>Relays: subscribeAndCollect(filter, timeout)
Relays-->>Strategy: events
Strategy->>Results: dedupe, sort, slice
Results-->>Orchestrator: NDKEvent[]
else reply not matched
Orchestrator->>Strategy: tryHandleRefSearch(...)
alt ref matched
Strategy->>Relays: subscribeAndCollect(filter, timeout)
Relays-->>Strategy: events
Strategy->>Results: dedupe, sort, slice
Results-->>Orchestrator: NDKEvent[]
else
Orchestrator->>Strategy: tryHandleLinkSearch(...) -> tryHandleDTagSearch(...)
end
end
end
Orchestrator-->>Client: first non-null NDKEvent[] or empty array
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/lib/search/strategies/linkSearchStrategy.ts (1)
43-56: Consider extracting shared fetch/dedup/sort logic.All four new strategies (
reply,ref,link,dtag) share nearly identical code for relay selection, fetching with fallback, Map-based deduplication, sorting, and slicing. This could be extracted into a shared helper to reduce duplication (~15 lines per strategy).This is a low-priority refactor since each strategy is self-contained and the current approach is clear.
♻️ Example shared helper
// In a shared module like searchUtils.ts export async function fetchAndDedupeEvents( filter: NDKFilter, chosenRelaySet: NDKRelaySet, hasSearchTerm: boolean, abortSignal?: AbortSignal, limit: number ): Promise<NDKEvent[]> { const relaySet = hasSearchTerm ? chosenRelaySet : await getBroadRelaySet(); let results: NDKEvent[]; try { results = await subscribeAndCollect(filter, 10000, relaySet, abortSignal); } catch { results = await subscribeAndCollect(filter, 10000, chosenRelaySet, abortSignal); } const dedupe = new Map<string, NDKEvent>(); for (const e of results) if (!dedupe.has(e.id)) dedupe.set(e.id, e); return sortEventsNewestFirst(Array.from(dedupe.values())).slice(0, limit); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/search/strategies/linkSearchStrategy.ts` around lines 43 - 56, The repeated relay selection, fetching-with-fallback, Map deduplication, sorting and slicing logic in the strategies should be moved into a shared helper (e.g., fetchAndDedupeEvents) and invoked from reply/ref/link/dtag to remove duplication; implement a function that accepts (filter: NDKFilter, chosenRelaySet: NDKRelaySet, hasSearchTerm: boolean, abortSignal?: AbortSignal, limit: number), uses getBroadRelaySet when !hasSearchTerm, calls subscribeAndCollect with the two-tier fallback, builds a Map by event.id to dedupe, applies sortEventsNewestFirst, and returns .slice(0, limit), then replace the inline code in each strategy with a call to this helper.src/lib/examples.ts (1)
156-158: Consider adding examples forref:anddtag:keywords.The PR adds four new keywords, but only
reply:andlink:have examples here. Adding examples forref:(e.g.,ref:30023:pubkey:article-slug) anddtag:would improve discoverability of all the new features.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/examples.ts` around lines 156 - 158, Update the tag filter examples array in src/lib/examples.ts to include sample entries for the new keywords: add a 'ref:' example (e.g., "ref:30023:pubkey:article-slug" to show chainId:eventType:pubkey:slug format) and a 'dtag:' example (e.g., "dtag:alice" or "dtag:alice:domain" to show tagged-identifier usage) alongside the existing 'reply:' and 'link:' items so the examples cover all four newly added keywords.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/lib/examples.ts`:
- Around line 156-158: Update the tag filter examples array in
src/lib/examples.ts to include sample entries for the new keywords: add a 'ref:'
example (e.g., "ref:30023:pubkey:article-slug" to show
chainId:eventType:pubkey:slug format) and a 'dtag:' example (e.g., "dtag:alice"
or "dtag:alice:domain" to show tagged-identifier usage) alongside the existing
'reply:' and 'link:' items so the examples cover all four newly added keywords.
In `@src/lib/search/strategies/linkSearchStrategy.ts`:
- Around line 43-56: The repeated relay selection, fetching-with-fallback, Map
deduplication, sorting and slicing logic in the strategies should be moved into
a shared helper (e.g., fetchAndDedupeEvents) and invoked from
reply/ref/link/dtag to remove duplication; implement a function that accepts
(filter: NDKFilter, chosenRelaySet: NDKRelaySet, hasSearchTerm: boolean,
abortSignal?: AbortSignal, limit: number), uses getBroadRelaySet when
!hasSearchTerm, calls subscribeAndCollect with the two-tier fallback, builds a
Map by event.id to dedupe, applies sortEventsNewestFirst, and returns .slice(0,
limit), then replace the inline code in each strategy with a call to this
helper.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 9dbcad1f-d4d5-4a53-b56b-7f3539ad5528
📒 Files selected for processing (8)
src/lib/examples.tssrc/lib/search/contentFilter.tssrc/lib/search/searchOrchestrator.tssrc/lib/search/strategies/dTagSearchStrategy.tssrc/lib/search/strategies/linkSearchStrategy.tssrc/lib/search/strategies/refSearchStrategy.tssrc/lib/search/strategies/replySearchStrategy.tstsconfig.json
Supports id:<hex>, id:<note1...>, id:<nevent1...> to fetch specific events by ID. Multiple id: tokens supported. Placed first in orchestrator since it's a direct lookup. Part of #201
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/lib/search/searchOrchestrator.ts (1)
20-21: Align function contract comment with actual return behavior.The doc says “first non-null/non-empty result”, but guards return on any non-null value (including
[]). Update wording to avoid ambiguity.Proposed doc wording
- * Run search strategies in order and return the first non-null/non-empty result + * Run search strategies in order and return the first non-null result🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/search/searchOrchestrator.ts` around lines 20 - 21, The function comment for searchOrchestrator is misleading: the implementation returns on the first non-null value (it does not treat empty arrays as empty results), so update the docstring for the searchOrchestrator function to state that it returns the first non-null result (including empty arrays) and returns null only if every strategy returns null; mention "non-null" explicitly rather than "non-null/non-empty" to match the behavior of the existing guards.src/lib/search/strategies/idSearchStrategy.ts (1)
40-41: Deduplicate resolved event IDs, not just raw tokens.Different token forms (
hex,note1...,nevent1...) can resolve to the same id and produce duplicateidsentries in the relay filter.Proposed dedupe/type-safety cleanup
- const eventIds = tokens.map(resolveEventId).filter(Boolean) as string[]; + const eventIds = Array.from( + new Set(tokens.map(resolveEventId).filter((id): id is string => Boolean(id))) + );As per coding guidelines,
src/lib/search/**: “Performance: avoid O(n²), prefer Set-based dedup, parallel fetches”.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/search/strategies/idSearchStrategy.ts` around lines 40 - 41, The current code deduplicates raw tokens (tokens) before calling resolveEventId, which can yield duplicate event IDs when different token forms map to the same id; change the flow in idSearchStrategy.ts to first map idMatches through resolveEventId (use idMatches.map(m => resolveEventId(m[1]))), filter out falsy results, then use a Set to deduplicate the resolved event IDs and convert back to an array (eventIds) so the relay filter contains unique IDs; ensure the resulting eventIds is typed as string[] and keep the operations Set-based to avoid O(n²).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/lib/search/strategies/idSearchStrategy.ts`:
- Around line 45-52: The id lookup currently assumes getBroadRelaySet() succeeds
and returns [] on subscribeAndCollect failures without attempting the same
lookup against the chosen relays; update idSearchStrategy so it first wraps
getBroadRelaySet() in try/catch and falls back to chosenRelaySet when
getBroadRelaySet() throws, and if subscribeAndCollect(filter, 10000, relaySet,
abortSignal) fails (catch block) retry subscribeAndCollect once more using
chosenRelaySet before settling on an empty results array; reference
getBroadRelaySet, subscribeAndCollect, chosenRelaySet and the results variable
in your changes to locate and implement the fallback logic.
---
Nitpick comments:
In `@src/lib/search/searchOrchestrator.ts`:
- Around line 20-21: The function comment for searchOrchestrator is misleading:
the implementation returns on the first non-null value (it does not treat empty
arrays as empty results), so update the docstring for the searchOrchestrator
function to state that it returns the first non-null result (including empty
arrays) and returns null only if every strategy returns null; mention "non-null"
explicitly rather than "non-null/non-empty" to match the behavior of the
existing guards.
In `@src/lib/search/strategies/idSearchStrategy.ts`:
- Around line 40-41: The current code deduplicates raw tokens (tokens) before
calling resolveEventId, which can yield duplicate event IDs when different token
forms map to the same id; change the flow in idSearchStrategy.ts to first map
idMatches through resolveEventId (use idMatches.map(m => resolveEventId(m[1]))),
filter out falsy results, then use a Set to deduplicate the resolved event IDs
and convert back to an array (eventIds) so the relay filter contains unique IDs;
ensure the resulting eventIds is typed as string[] and keep the operations
Set-based to avoid O(n²).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 4a8bb5cc-9ae6-4a63-a468-128a2d5d2a5f
📒 Files selected for processing (4)
src/lib/search/contentFilter.tssrc/lib/search/searchOrchestrator.tssrc/lib/search/strategies/dTagSearchStrategy.tssrc/lib/search/strategies/idSearchStrategy.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/lib/search/strategies/dTagSearchStrategy.ts
- src/lib/search/contentFilter.ts
| const relaySet = await getBroadRelaySet(); | ||
|
|
||
| let results: NDKEvent[]; | ||
| try { | ||
| results = await subscribeAndCollect(filter, 10000, relaySet, abortSignal); | ||
| } catch { | ||
| results = []; | ||
| } |
There was a problem hiding this comment.
Add relay fallback for id lookups when broad relay resolution/fetch fails.
id: search currently returns [] on fetch error and can throw if getBroadRelaySet() fails, so it does not degrade like other tag strategies that fallback to chosenRelaySet.
Proposed resilience fix
export async function tryHandleIdSearch(
cleanedQuery: string,
context: SearchContext
): Promise<NDKEvent[] | null> {
- const { abortSignal, limit } = context;
+ const { abortSignal, limit, chosenRelaySet } = context;
@@
- const relaySet = await getBroadRelaySet();
+ let relaySet = chosenRelaySet;
+ try {
+ relaySet = await getBroadRelaySet();
+ } catch {
+ relaySet = chosenRelaySet;
+ }
@@
let results: NDKEvent[];
try {
results = await subscribeAndCollect(filter, 10000, relaySet, abortSignal);
} catch {
- results = [];
+ if (relaySet !== chosenRelaySet) {
+ try {
+ results = await subscribeAndCollect(filter, 10000, chosenRelaySet, abortSignal);
+ } catch {
+ results = [];
+ }
+ } else {
+ results = [];
+ }
}As per coding guidelines, src/lib/search/**: “Graceful degradation when relays are unreachable”.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/search/strategies/idSearchStrategy.ts` around lines 45 - 52, The id
lookup currently assumes getBroadRelaySet() succeeds and returns [] on
subscribeAndCollect failures without attempting the same lookup against the
chosen relays; update idSearchStrategy so it first wraps getBroadRelaySet() in
try/catch and falls back to chosenRelaySet when getBroadRelaySet() throws, and
if subscribeAndCollect(filter, 10000, relaySet, abortSignal) fails (catch block)
retry subscribeAndCollect once more using chosenRelaySet before settling on an
empty results array; reference getBroadRelaySet, subscribeAndCollect,
chosenRelaySet and the results variable in your changes to locate and implement
the fallback logic.
- Extract shared fetch/dedup/sort logic into strategyUtils.ts - Dedupe resolved event IDs (not just raw tokens) in id strategy - Add fallback relay handling in shared helper - Fix orchestrator docstring (non-null, not non-null/non-empty) - Add d: example to examples.ts - Rename dtag: to d: in all strategies
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/lib/search/strategies/refSearchStrategy.ts (1)
11-21: Verify NIP-33 coordinate regex handles edge cases.The regex
/^\d+:[0-9a-f]{64}:/correctly validates the basic structure, but note:
- It requires lowercase hex only (
[0-9a-f]), while input could contain uppercase hex- This means
30023:ABCD...would fail validation and fall through to naddr decodingIf uppercase hex coordinates are valid input (they technically are valid hex), consider case-insensitive matching:
💡 Optional fix for case-insensitive hex
function resolveATagCoordinate(token: string): string | null { - if (/^\d+:[0-9a-f]{64}:/.test(token)) return token; + if (/^\d+:[0-9a-fA-F]{64}:/i.test(token)) return token.toLowerCase();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/search/strategies/refSearchStrategy.ts` around lines 11 - 21, The regex in resolveATagCoordinate only accepts lowercase hex and will reject valid coordinates with uppercase letters; update the validation to be case-insensitive (e.g., add the regex "i" flag or expand to [0-9a-fA-F]) so tokens like "30023:ABCD...:" are accepted, while keeping the existing nip19.decode fallback and return behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/lib/search/strategies/refSearchStrategy.ts`:
- Around line 11-21: The regex in resolveATagCoordinate only accepts lowercase
hex and will reject valid coordinates with uppercase letters; update the
validation to be case-insensitive (e.g., add the regex "i" flag or expand to
[0-9a-fA-F]) so tokens like "30023:ABCD...:" are accepted, while keeping the
existing nip19.decode fallback and return behavior unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 7c89d80f-29ef-43b5-89f5-12ae18842f0c
📒 Files selected for processing (8)
src/lib/examples.tssrc/lib/search/searchOrchestrator.tssrc/lib/search/strategies/dTagSearchStrategy.tssrc/lib/search/strategies/idSearchStrategy.tssrc/lib/search/strategies/linkSearchStrategy.tssrc/lib/search/strategies/refSearchStrategy.tssrc/lib/search/strategies/replySearchStrategy.tssrc/lib/search/strategies/strategyUtils.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- src/lib/search/searchOrchestrator.ts
- src/lib/search/strategies/replySearchStrategy.ts
- src/lib/search/strategies/linkSearchStrategy.ts
- src/lib/search/strategies/idSearchStrategy.ts
|
What are the Top5 things we could improve in regards to the things we implemented on this branch? |
|
Top 5 improvements for the keywords on this branch: 1. 2. 3. Combined keyword queries don't work yet 4. 5. No |
|
I think (3) and (4) are the most important. We should implement those now. |
All tag strategies (reply:, ref:, link:, d:) now parse by: tokens from the residual text and add them as an authors filter on the subscription. This means queries like 'reply:note1... by:dergigi' correctly find replies by that author. Extracted shared parseResidual() helper in strategyUtils.ts.
id: queries now bypass the full search pipeline (relay discovery, kind/date parsing, NIP-50 extensions). handleIdLookup() is a standalone function that only needs the raw query, abort signal, and limit — no SearchContext required. Also: all tag strategies now support by: combinations via shared parseResidual() helper.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/lib/search/strategies/idSearchStrategy.ts`:
- Around line 37-44: The call to getBroadRelaySet() in handleIdLookup can throw
and must be guarded; wrap getBroadRelaySet() in a try/catch and on error set
relaySet to a safe fallback (e.g., an empty array or a lightweight fallback
relay set) so the subsequent subscribeAndCollect(filter, 10000, relaySet,
abortSignal) call gracefully degrades instead of throwing; ensure the catch
preserves abortSignal behavior and logs or annotates the fallback so callers can
distinguish a degraded result.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: fb3cadf4-edff-430b-8b76-04ba93384fe1
📒 Files selected for processing (9)
src/lib/examples.tssrc/lib/search.tssrc/lib/search/searchOrchestrator.tssrc/lib/search/strategies/dTagSearchStrategy.tssrc/lib/search/strategies/idSearchStrategy.tssrc/lib/search/strategies/linkSearchStrategy.tssrc/lib/search/strategies/refSearchStrategy.tssrc/lib/search/strategies/replySearchStrategy.tssrc/lib/search/strategies/strategyUtils.ts
🚧 Files skipped from review as they are similar to previous changes (5)
- src/lib/examples.ts
- src/lib/search/strategies/linkSearchStrategy.ts
- src/lib/search/strategies/dTagSearchStrategy.ts
- src/lib/search/strategies/replySearchStrategy.ts
- src/lib/search/strategies/refSearchStrategy.ts
| const relaySet = await getBroadRelaySet(); | ||
|
|
||
| let results: NDKEvent[]; | ||
| try { | ||
| results = await subscribeAndCollect(filter, 10000, relaySet, abortSignal); | ||
| } catch { | ||
| results = []; | ||
| } |
There was a problem hiding this comment.
Wrap getBroadRelaySet() in try/catch for graceful degradation.
If getBroadRelaySet() throws (e.g., all relays are dead), the entire id: lookup will fail with an unhandled exception. Unlike fetchDedupeAndSort in strategyUtils.ts which handles this case, handleIdLookup lacks fallback protection.
🛡️ Proposed fix to add relay fallback
+import { NDKRelaySet } from '@nostr-dev-kit/ndk';
+import { ndk } from '../../ndk';
+import { RELAYS } from '../../constants';
+
export async function handleIdLookup(
query: string,
abortSignal?: AbortSignal,
limit: number = 200
): Promise<NDKEvent[] | null> {
const matches = Array.from(query.matchAll(/\bid:(\S+)/gi));
if (matches.length === 0) return null;
const eventIds = Array.from(
new Set(matches.map((m) => resolveEventId(m[1] || '')).filter((id): id is string => Boolean(id)))
);
if (eventIds.length === 0) return [];
const filter: NDKFilter = { ids: eventIds };
- const relaySet = await getBroadRelaySet();
+
+ let relaySet: NDKRelaySet;
+ try {
+ relaySet = await getBroadRelaySet();
+ } catch {
+ // Fallback to default relays if broad relay set fails
+ relaySet = NDKRelaySet.fromRelayUrls(RELAYS.DEFAULT, ndk);
+ }
let results: NDKEvent[];
try {
results = await subscribeAndCollect(filter, 10000, relaySet, abortSignal);
} catch {
results = [];
}As per coding guidelines, src/lib/search/**: "Graceful degradation when relays are unreachable".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/search/strategies/idSearchStrategy.ts` around lines 37 - 44, The call
to getBroadRelaySet() in handleIdLookup can throw and must be guarded; wrap
getBroadRelaySet() in a try/catch and on error set relaySet to a safe fallback
(e.g., an empty array or a lightweight fallback relay set) so the subsequent
subscribeAndCollect(filter, 10000, relaySet, abortSignal) call gracefully
degrades instead of throwing; ensure the catch preserves abortSignal behavior
and logs or annotates the fallback so callers can distinguish a degraded result.
Add missing query keywords for tag-based filtering, enabling full spell (NIP-A7) translation.
Resolves 201
New Keywords
reply:#ereply:note1...,reply:<hex>ref:#aref:naddr1...,ref:kind:pubkey:d-taglink:#rlink:https://github.com/nostr-protocol/nipsd:#dd:my-article-slugid:idsid:note1...,id:<hex>Complete REQ Filter → Ants Mapping
With these additions, every standard REQ filter field now has an ants keyword equivalent:
kindskind:/is:authorsby:idsid:#t#hashtag#pmentions:#ereply:#aref:#rlink:#dd:searchsince/untilsince:/until:limitDetails
reply:note1... reply:note1...reply:andid:accept hex,note1..., andnevent1...bech32ref:accepts raw coordinates andnaddr1...bech32fetchDedupeAndSort()helper extracts relay selection, fallback, dedup, and sortingSTRUCTURED_TOKEN_PATTERNin contentFilterSummary by CodeRabbit
New Features
Chores