Files
client/plans/fetchevents-ndk-migration.md
2026-04-17 16:52:51 -04:00

7.1 KiB

Migration Plan: Replace fetchEventsFromAllRelays with NDK fetchEvents

Problem

fetchEventsFromAllRelays() in www/js/init-ndk.mjs is a custom function that bypasses NDK entirely. It:

  1. Calls getRelayData() to get all relays (including write-only)
  2. Iterates over every relay and sends raw WebSocket REQ messages via handleFetchEventsFromRelay() in the worker
  3. Returns events grouped by relay: { "wss://relay1/": [...], "wss://relay2/": [...] }

This causes:

  • Write-only relays receive read requests (violates NIP-65)
  • No outbox model routing — NDK's smart relay selection is bypassed
  • No caching — results don't go through NDK's Dexie cache
  • Redundant deduplication — every caller does Object.values(result).flat() then deduplicates manually

Solution

Replace fetchEventsFromAllRelays with a new ndkFetchEvents() function that uses NDK's ndk.fetchEvents() internally. NDK handles relay routing, caching, and deduplication automatically.

Architecture

graph LR
    subgraph Before
        A[UI Page] -->|fetchEventsFromAllRelays| B[init-ndk.mjs]
        B -->|getRelayData| C[Worker]
        C -->|returns ALL relays| B
        B -->|fetchEvents per relay| D[Worker: raw WebSocket REQ]
        D -->|intercepts WS messages| E[Each Relay]
    end

    subgraph After
        F[UI Page] -->|ndkFetchEvents| G[init-ndk.mjs]
        G -->|ndkFetchEvents message| H[Worker]
        H -->|ndk.fetchEvents| I[NDK outbox routing]
        I -->|smart relay selection| J[Read/Both Relays Only]
    end

New API

Worker side (ndk-worker.js)

Add a new message handler ndkFetchEvents:

case 'ndkFetchEvents':
    await handleNdkFetchEvents(requestId, filters, port);
    break;
async function handleNdkFetchEvents(requestId, filters, port) {
    try {
        const eventSet = await ndk.fetchEvents(filters);
        const events = Array.from(eventSet).map(e => e.rawEvent());
        port.postMessage({
            type: 'ndkFetchEventsResult',
            requestId,
            events,
            error: null
        });
    } catch (err) {
        port.postMessage({
            type: 'ndkFetchEventsResult',
            requestId,
            events: [],
            error: err?.message || String(err)
        });
    }
}

UI side (init-ndk.mjs)

Add a new exported function:

export async function ndkFetchEvents(filters) {
    return new Promise((resolve, reject) => {
        const requestId = `ndkFetch_${Date.now()}_${++requestCounter}`;
        pendingRequests.set(requestId, { resolve, reject });
        setTimeout(() => {
            if (pendingRequests.has(requestId)) {
                pendingRequests.delete(requestId);
                reject(new Error('ndkFetchEvents timeout'));
            }
        }, 15000);
        ndkWorker.port.postMessage({
            type: 'ndkFetchEvents',
            requestId,
            filters
        });
    });
}

Returns: Array<RawEvent> — a flat, deduplicated array of raw nostr events.

Response handler in init-ndk.mjs

} else if (message.type === 'ndkFetchEventsResult') {
    const pending = pendingRequests.get(message.requestId);
    if (pending) {
        pendingRequests.delete(message.requestId);
        if (message.error) {
            pending.reject(new Error(message.error));
        } else {
            pending.resolve(message.events || []);
        }
    }
}

Migration Per File

Every call site currently does:

const eventsByRelay = await fetchEventsFromAllRelays(filters);
const events = Object.values(eventsByRelay).flat();

After migration:

const events = await ndkFetchEvents(filters);

Files to migrate

File Call sites Notes
www/js/init-ndk.mjs Definition of fetchEventsFromAllRelays Keep for event.html, add ndkFetchEvents
www/js/profile-cache.mjs fetchEventsFromAllRelaysFn(filters) Replace option name to ndkFetchEvents
www/js/post-interactions.mjs fetchEventsFromAllRelaysFn(filter), fetchExistingInteractions Replace option + internal calls
www/js/post-interactions2.mjs Imports and passes fetchEventsFromAllRelays Update imports to ndkFetchEvents
www/feed.html fetchEventsFromAllRelays(filters) Direct replacement
www/post.html fetchEventsFromAllRelays(filters) Direct replacement
www/post2.html Multiple calls Direct replacement, remove .flat() patterns
www/notifications.html Multiple calls Direct replacement
www/people.html Multiple calls Direct replacement
www/cashu.html 3 parallel calls Direct replacement
www/msg.html 1 call Direct replacement
www/ai.html 1 call Direct replacement
www/event.html Uses fetchEventsFromAllRelays Keep as-is (needs per-relay attribution)

Detailed call-site transformations

Pattern A: Simple fetch + flatten (most common)

// BEFORE
const eventsByRelay = await fetchEventsFromAllRelays(filters);
const events = Object.values(eventsByRelay).flat();

// AFTER
const events = await ndkFetchEvents(filters);

Pattern B: Parallel with queryCache

// BEFORE (feed.html, post.html)
const [cached, freshByRelay] = await Promise.all([
    queryCache(filters).catch(() => []),
    fetchEventsFromAllRelays(filters).catch(() => ({}))
]);

// AFTER
const [cached, fresh] = await Promise.all([
    queryCache(filters).catch(() => []),
    ndkFetchEvents(filters).catch(() => [])
]);
// Note: NDK fetchEvents already checks cache, so queryCache may be redundant
// but keeping it for now ensures backward compatibility

Pattern C: Passed as option/callback

// BEFORE (post-interactions.mjs, profile-cache.mjs)
fetchExistingInteractions(postIds, { fetchEventsFromAllRelays, ... });

// AFTER
fetchExistingInteractions(postIds, { ndkFetchEvents, ... });
// Internal: const events = await ndkFetchEvents(filters);
// (no more Object.values().flat())

Execution Order

  1. Add worker handlerhandleNdkFetchEvents in ndk-worker.js
  2. Add UI functionndkFetchEvents + response handler in init-ndk.mjs
  3. Migrate shared modulesprofile-cache.mjs, post-interactions.mjs, post-interactions2.mjs
  4. Migrate pages one at a timefeed.html, post.html, post2.html, notifications.html, people.html, cashu.html, msg.html, ai.html
  5. Keep event.html unchanged — it needs per-relay attribution
  6. Clean up — remove unused fetchEventsFromAllRelays export (keep only for event.html import)

Risk Mitigation

  • NDK's fetchEvents returns Set<NDKEvent> which we convert to raw events via .rawEvent() — this preserves the same data shape callers expect
  • The 15-second timeout on ndkFetchEvents is generous; NDK handles per-relay timeouts internally
  • If NDK's outbox model hasn't loaded relay lists yet, it falls back to explicit pool relays (same as current behavior for read/both relays)
  • queryCache calls alongside ndkFetchEvents are redundant (NDK checks cache first) but harmless — can be removed in a follow-up cleanup