@ -7,9 +7,11 @@
@@ -7,9 +7,11 @@
import { page } from '$app/stores';
import type { NostrEvent } from '../../../lib/types/nostr.js';
import { goto } from '$app/navigation';
import { nip19 } from 'nostr-tools';
let events = $state< NostrEvent [ ] > ([]);
let loading = $state(true);
let error = $state< string | null > (null);
let dTag = $derived($page.params.d_tag);
onMount(async () => {
@ -27,75 +29,130 @@
@@ -27,75 +29,130 @@
if (!dTag) return;
loading = true;
error = null;
try {
const relays = relayManager.getProfileReadRelays();
// Fetch all replaceable events with matching d-tag
// Use range queries which are more efficient than listing all kinds
// Most relays support range queries or we can use multiple filters
// Use both feed and profile relays for wider coverage
const profileRelays = relayManager.getProfileReadRelays();
const feedRelays = relayManager.getFeedReadRelays();
const relays = [...new Set([...profileRelays, ...feedRelays])]; // Deduplicate
const allEvents: NostrEvent[] = [];
// Query basic replaceable kinds (0, 3)
const basicKinds = [0, 3];
const basicEvents = await nostrClient.fetchEvents(
[{ kinds : basicKinds , '#d' : [ dTag ], limit : 100 } ],
relays,
{ useCache : true , cacheResults : true }
);
allEvents.push(...basicEvents);
// Query replaceable range (10000-19999) - use a single filter with all kinds
// If relay doesn't support large kind lists, it will return an error and we skip
// First, check cache for events with this d-tag
try {
const replaceableKinds: number[] = [];
for (let kind = 10000; kind < 20000 ; kind ++) {
replaceableKinds.push(kind);
}
const replaceableEvents = await nostrClient.fetchEvents(
[{ kinds : replaceableKinds , '#d' : [ dTag ], limit : 100 } ],
relays,
{ useCache : true , cacheResults : true }
);
allEvents.push(...replaceableEvents);
} catch (rangeError) {
// If single query fails (relay limit), fall back to smaller batches
const BATCH_SIZE = 1000;
for (let start = 10000; start < 20000 ; start += BATCH_SIZE ) {
const batchKinds: number[] = [];
for (let kind = start; kind < Math.min ( start + BATCH_SIZE , 20000 ); kind ++) {
batchKinds.push(kind);
const { getEventsByKind } = await import('../../../lib/services/cache/event-cache.js');
// Check parameterized replaceable range in cache
// Query each kind individually since getEventsByKind takes a single kind number
for (let kind = 30000; kind < 40000 ; kind ++) {
const cached = await getEventsByKind(kind);
const matching = cached.filter(e => {
const eventDTag = e.tags.find(t => t[0] === 'd')?.[1] || '';
return eventDTag === dTag;
});
if (matching.length > 0) {
allEvents.push(...matching);
console.log(`[Replaceable] Found ${ matching . length } cached events with d-tag "${ dTag } " for kind ${ kind } `);
}
}
} catch (cacheError) {
console.warn('[Replaceable] Error checking cache:', cacheError);
}
try {
const batchEvents = await nostrClient.fetchEvents(
[{ kinds : batchKinds , '#d' : [ dTag ], limit : 100 } ],
relays,
{ useCache : true , cacheResults : true }
);
allEvents.push(...batchEvents);
} catch {
// Skip failed batches
// First, try to decode as naddr (if the d-tag is actually an naddr)
let decodedNaddr: { kind : number ; pubkey : string ; identifier? : string ; relays? : string [] } | null = null;
if (/^naddr1[a-z0-9]+$/i.test(dTag)) {
try {
const decoded = nip19.decode(dTag);
if (decoded.type === 'naddr' && decoded.data && typeof decoded.data === 'object' && 'kind' in decoded.data && 'pubkey' in decoded.data) {
decodedNaddr = decoded.data as { kind : number ; pubkey : string ; identifier? : string ; relays? : string [] } ;
console.log('[Replaceable] Decoded naddr:', decodedNaddr);
}
} catch (e) {
console.log('[Replaceable] Not an naddr, treating as d-tag:', e);
}
}
// Query parameterized replaceable range (30000-39999)
try {
const paramReplaceableKinds: number[] = [];
for (let kind = 30000; kind < 40000 ; kind ++) {
paramReplaceableKinds.push(kind);
// If we decoded an naddr, fetch directly by kind, pubkey, and d-tag
if (decodedNaddr) {
const naddrRelays = decodedNaddr.relays & & decodedNaddr.relays.length > 0
? decodedNaddr.relays
: relays;
const filter: any = {
kinds: [decodedNaddr.kind],
authors: [decodedNaddr.pubkey],
limit: 1
};
if (decodedNaddr.identifier) {
filter['#d'] = [decodedNaddr.identifier];
}
const paramEvents = await nostrClient.fetchEvents(
[{ kinds : paramReplaceableKinds , '#d' : [ dTag ], limit : 100 } ],
relays,
const naddr Events = await nostrClient.fetchEvents(
[filter ],
nadd rR elays,
{ useCache : true , cacheResults : true }
);
allEvents.push(...paramEvents);
} catch (rangeError) {
// If single query fails, fall back to smaller batches
const BATCH_SIZE = 1000;
if (naddrEvents.length > 0) {
events = naddrEvents;
loading = false;
return;
} else {
error = `Event not found for naddr. Tried kind ${ decodedNaddr . kind } , pubkey ${ decodedNaddr . pubkey . substring ( 0 , 16 )} ..., d-tag: ${ decodedNaddr . identifier || 'none' } `;
}
}
// If not an naddr or naddr lookup failed, search by d-tag
// Focus on parameterized replaceable events (30000-39999) first since they commonly use d-tags
console.log('[Replaceable] Searching for events with d-tag:', dTag);
// Query parameterized replaceable range (30000-39999) - these are most likely to have d-tags
// Use smaller batches to avoid relay limits
const BATCH_SIZE = 100;
for (let start = 30000; start < 40000 ; start += BATCH_SIZE ) {
const batchKinds: number[] = [];
for (let kind = start; kind < Math.min ( start + BATCH_SIZE , 40000 ); kind ++) {
batchKinds.push(kind);
}
try {
const batchEvents = await nostrClient.fetchEvents(
[{ kinds : batchKinds , '#d' : [ dTag ], limit : 100 } ],
relays,
{ useCache : true , cacheResults : true , timeout : 10000 }
);
allEvents.push(...batchEvents);
console.log(`[Replaceable] Found ${ batchEvents . length } events in kind range ${ start } -${ Math . min ( start + BATCH_SIZE - 1 , 39999 )} `);
} catch (e) {
console.warn(`[Replaceable] Failed to query kind range ${ start } -${ Math . min ( start + BATCH_SIZE - 1 , 39999 )} :`, e);
}
}
// If we found events in cache, extract relay hints from their tags and query those relays too
const additionalRelays = new Set< string > ();
for (const event of allEvents) {
// Extract relay hints from r tags
const rTags = event.tags.filter(t => t[0] === 'r' & & t[1]);
for (const rTag of rTags) {
if (rTag[1] && (rTag[1].startsWith('ws://') || rTag[1].startsWith('wss://'))) {
additionalRelays.add(rTag[1]);
}
}
// Extract relay hints from a tags (third element is often a relay)
const aTags = event.tags.filter(t => t[0] === 'a' & & t.length > 2);
for (const aTag of aTags) {
if (aTag[2] && (aTag[2].startsWith('ws://') || aTag[2].startsWith('wss://'))) {
additionalRelays.add(aTag[2]);
}
}
}
// Query additional relays if we found any
if (additionalRelays.size > 0) {
const additionalRelaysArray = Array.from(additionalRelays);
console.log(`[Replaceable] Querying ${ additionalRelaysArray . length } additional relays from event hints:`, additionalRelaysArray);
// Query parameterized replaceable range on additional relays
for (let start = 30000; start < 40000 ; start += BATCH_SIZE ) {
const batchKinds: number[] = [];
for (let kind = start; kind < Math.min ( start + BATCH_SIZE , 40000 ); kind ++) {
@ -105,16 +162,31 @@
@@ -105,16 +162,31 @@
try {
const batchEvents = await nostrClient.fetchEvents(
[{ kinds : batchKinds , '#d' : [ dTag ], limit : 100 } ],
relays ,
{ useCache : true , cacheResults : true }
additionalRelaysArray ,
{ useCache : true , cacheResults : true , timeout : 10000 }
);
allEvents.push(...batchEvents);
} catch {
// Skip failed batches
console.log(`[Replaceable] Found ${ batchEvents . length } additional events from hint relays in kind range ${ start } -${ Math . min ( start + BATCH_SIZE - 1 , 39999 )} `);
} catch (e) {
console.warn(`[Replaceable] Failed to query hint relays for kind range ${ start } -${ Math . min ( start + BATCH_SIZE - 1 , 39999 )} :`, e);
}
}
}
if (allEvents.length > 0) {
console.log(`[Replaceable] Found ${ allEvents . length } events total (including cache and hint relays)`);
}
// Also check common replaceable kinds that might have d-tags
const commonKinds = [0, 3, 10000, 10001, 10002, 10003, 10004, 10005, 10006, 10007, 10008, 10009, 10010, 10030, 10031, 10032, 10033, 10034, 10035, 10036, 10037, 10038, 10039, 10040, 10041, 10042, 10043, 10044, 10045, 10046, 10047, 10048, 10049, 10050, 10133, 10432, 30315];
const commonEvents = await nostrClient.fetchEvents(
[{ kinds : commonKinds , '#d' : [ dTag ], limit : 100 } ],
relays,
{ useCache : true , cacheResults : true }
);
allEvents.push(...commonEvents);
console.log(`[Replaceable] Found ${ commonEvents . length } events in common replaceable kinds`);
// For replaceable events, get the newest version of each (by pubkey and kind)
// For parameterized replaceable, get newest by (pubkey, kind, d-tag)
const eventsByKey = new Map< string , NostrEvent > ();
@ -134,8 +206,15 @@
@@ -134,8 +206,15 @@
// Sort by created_at descending
events = Array.from(eventsByKey.values()).sort((a, b) => b.created_at - a.created_at);
} catch (error) {
console.error('Error loading replaceable events:', error);
console.log(`[Replaceable] Total unique events found: ${ events . length } `);
if (events.length === 0 && !error) {
error = `No replaceable events found with d-tag "${ dTag } ". The event might not be on the queried relays, or the d-tag might be incorrect.`;
}
} catch (err) {
console.error('Error loading replaceable events:', err);
error = err instanceof Error ? err.message : 'Failed to load replaceable events';
events = [];
} finally {
loading = false;
@ -152,7 +231,7 @@
@@ -152,7 +231,7 @@
< main class = "container mx-auto px-4 py-8" >
< div class = "replaceable-content" >
< div class = "replaceable-header mb-6" >
< h1 class = "text-2xl font-bold text-fog-text dark:text-fog-dark-text" >
< h1 class = "font-bold text-fog-text dark:text-fog-dark-text font-mono mb-6" style = "font-size: 1.5em; " >
Replaceable Events: { dTag }
< / h1 >
< p class = "text-fog-text-light dark:text-fog-dark-text-light mt-2" >
@ -166,7 +245,14 @@
@@ -166,7 +245,14 @@
< / div >
{ :else if events . length === 0 }
< div class = "empty-state" >
< p class = "text-fog-text dark:text-fog-dark-text" > No replaceable events found with this d-tag.< / p >
< p class = "text-fog-text dark:text-fog-dark-text" >
{ error || 'No replaceable events found with this d-tag.' }
< / p >
{ #if dTag && ! dTag . startsWith ( 'naddr1' )}
< p class = "text-fog-text-light dark:text-fog-dark-text-light mt-2 text-sm" >
Tip: If you have an naddr, you can use it directly: < code > /replaceable/naddr1...< / code >
< / p >
{ /if }
< / div >
{ : else }
< div class = "events-list" >