# Broadcast Relay Batching Fix

## Problem

With 600+ broadcast relays, NDK's `fromRelayUrls` creates 630 temporary relays
and adds them all to the pool simultaneously. `NDKRelaySet.publish()` then fires
all 630 relay publish operations in parallel via `Promise.all`. Each relay that
isn't connected calls `relay.connect()` which opens a WebSocket.

Browsers limit simultaneous WebSocket connections (practically ~50-100 at a
time before connections queue up). With 630 simultaneous connection attempts,
most relays never get a connection slot before the per-relay timeout expires.
Result: only ~13/630 relays succeed.

## Solution: Chunked publishing

Instead of creating one giant `NDKRelaySet` with all 630 relays and calling
`publish()` once, we split the relay URLs into **batches of 50** and publish
to each batch sequentially. Each batch gets its own `NDKRelaySet.fromRelayUrls()`
call, its own `publish()`, and its own set of live progress listeners.

```mermaid
flowchart TD
  A[630 relay URLs] --> B[Split into batches of 50]
  B --> C[Batch 1: 50 relays]
  C --> D[publish to batch 1]
  D --> E{Batch 1 done?}
  E -->|yes| F[Batch 2: 50 relays]
  F --> G[publish to batch 2]
  G --> H{Batch 2 done?}
  H -->|yes| I[... continue ...]
  I --> J[Batch 13: 30 relays]
  J --> K[publish to batch 13]
  K --> L{All batches done?}
  L -->|yes| M[Final done event with total count]
```

### Key design points

1. **Batch size: 50 relays** — small enough to stay within browser WebSocket
   limits, large enough to finish in a reasonable time. Each batch takes ~5-10s.

2. **Sequential batches** — wait for each batch to complete before starting the
   next. This keeps the total simultaneous connections at ~50, not 630.

3. **Live progress across batches** — the `publishedSoFar` and `failedSoFar`
   sets accumulate across all batches. The `broadcastProgress` events stream
   continuously, so the footer shows the count climbing from 0 to 630 across
   all batches.

4. **Per-relay timeout: 10s per batch** — each relay in a batch gets 10s to
   connect + publish. Since only 50 are competing for connections (not 630),
   this is plenty of time.

5. **Overall deadline: removed** — since batches are sequential and each has
   its own per-relay timeout, the overall time is naturally bounded at
   `numBatches × perBatchTimeout`. No need for a separate overall deadline.

6. **Connection timeout: 10s** — set on each relay in the batch, matching the
   publish timeout.

## Implementation (in `www/ndk-worker.js` `handlePublish`)

Replace the current single `NDKRelaySet.fromRelayUrls(allUrls, ndk)` + single
`publish()` with a batched loop:

```js
const BATCH_SIZE = 50;
const PER_RELAY_TIMEOUT_MS = 10000;

if (isBroadcast) {
    const allUrls = [...new Set([...outboxWriteUrls, ...activeBroadcastUrls])];
    const batches = [];
    for (let i = 0; i < allUrls.length; i += BATCH_SIZE) {
        batches.push(allUrls.slice(i, i + BATCH_SIZE));
    }
    console.log(`[Worker] Broadcasting to ${allUrls.length} relays in ${batches.length} batches of ${BATCH_SIZE}`);

    // Emit start event
    broadcast({ type: 'broadcastProgress', sessionId, eventId, eventKind, phase: 'start', total: allUrls.length, successful: 0, failed: 0, latestRelay: null });

    // Attach listeners once — they accumulate across all batches
    ndkEvent.on('relay:published', (relay) => { ... publishedSoFar.add ... });
    ndkEvent.on('relay:publish:failed', (relay, err) => { ... failedSoFar.add ... });

    // Publish to each batch sequentially
    for (let i = 0; i < batches.length; i++) {
        const batch = batches[i];
        const batchSet = NDKRelaySet.fromRelayUrls(batch, ndk);
        // Set connection timeout on each relay in this batch
        for (const relay of batchSet.relays) {
            try { relay.connectionTimeout = PER_RELAY_TIMEOUT_MS; } catch (_) {}
        }
        try {
            await ndkEvent.publish(batchSet, PER_RELAY_TIMEOUT_MS, 1);
        } catch (e) {
            // Expected — some relays in this batch may fail
        }
        console.log(`[Worker] Batch ${i+1}/${batches.length} done: ${publishedSoFar.size}/${allUrls.length} total succeeded`);
    }

    // Emit final done event
    broadcast({ type: 'broadcastProgress', sessionId, eventId, eventKind, phase: 'done', total: allUrls.length, successful: publishedSoFar.size, failed: failedSoFar.size + timedOutSoFar.size, relayUrls: Array.from(publishedSoFar) });
    // Clean up listeners
    ndkEvent.removeAllListeners('relay:published');
    ndkEvent.removeAllListeners('relay:publish:failed');
}
```

## Files touched

| File | Change |
|------|--------|
| [`www/ndk-worker.js`](www/ndk-worker.js) | Replace single publish with batched loop in `handlePublish` |

## Testing

1. Post with a user that has 600+ broadcast relays
2. Watch the footer: count should climb steadily as batches complete
3. Final count should be much higher than 13 (ideally 500+)
4. Each batch of 50 should take ~5-10 seconds
5. Total time for 630 relays: ~13 batches × ~10s = ~130 seconds
