# 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

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

```javascript
case 'ndkFetchEvents':
    await handleNdkFetchEvents(requestId, filters, port);
    break;
```

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

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

```javascript
} 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:
```javascript
const eventsByRelay = await fetchEventsFromAllRelays(filters);
const events = Object.values(eventsByRelay).flat();
```

After migration:
```javascript
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)
```javascript
// BEFORE
const eventsByRelay = await fetchEventsFromAllRelays(filters);
const events = Object.values(eventsByRelay).flat();

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

#### Pattern B: Parallel with queryCache
```javascript
// 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
```javascript
// 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 handler** — `handleNdkFetchEvents` in `ndk-worker.js`
2. **Add UI function** — `ndkFetchEvents` + response handler in `init-ndk.mjs`
3. **Migrate shared modules** — `profile-cache.mjs`, `post-interactions.mjs`, `post-interactions2.mjs`
4. **Migrate pages one at a time** — `feed.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
