186 lines
12 KiB
Markdown
186 lines
12 KiB
Markdown
# Relay Disable/Enable Feature Plan
|
|
|
|
## Goal
|
|
Allow users to **temporarily disable and enable individual relays** in the app without modifying the contents of `kind 10002` (the user's persisted relay list). This is an app-wide, session-level state that affects all pages via the shared NDK worker.
|
|
|
|
## Current State
|
|
|
|
### What exists today
|
|
- **`relays.html`** — clicking the "connected" status icon calls [`handleRelayReconnect(relayUrl)`](../www/relays.html:1440) which calls [`reconnectRelay(relayUrl)`](../www/js/init-ndk.mjs:1696) → sends `reconnectRelay` message to worker
|
|
- **Worker [`handleReconnectRelay()`](../www/ndk-worker.js:96970)** — disconnects the relay, waits 500ms, then reconnects it (a toggle/restart behavior)
|
|
- **Worker [`handleDisconnect()`](../www/ndk-worker.js:96802)** — disconnects ALL relays (used for logout only)
|
|
- **NDK auto-reconnect** — when a relay disconnects, NDK's connectivity layer automatically attempts reconnection with exponential backoff (see `handleReconnection()` at [line 19884](../www/ndk-worker.js:19884) and flapping detection at [line 19844](../www/ndk-worker.js:19844))
|
|
|
|
### What's missing
|
|
There is **no concept of "temporarily disabled"** — the only options are:
|
|
1. Reconnect (disconnect + immediate reconnect) — relay comes right back up
|
|
2. Full disconnect (logout) — disconnects everything
|
|
|
|
If you just call `relay.disconnect()`, NDK's auto-reconnect logic will bring it back online within seconds.
|
|
|
|
### The outbox model
|
|
The worker's NDK instance has a full outbox model ([`ndk.outboxTracker`](../www/ndk-worker.js:54365), [`ndk.outboxPool`](../www/ndk-worker.js:54759)) that:
|
|
- Resolves followed authors' `kind 10002` relay lists
|
|
- Adds **temporary relays** to the pool via `pool.useTemporaryRelay()` to fetch events from those authors
|
|
- These temporary relays auto-remove after inactivity (`temporaryRelayTimers`)
|
|
|
|
NDK has a built-in **`relayConnectionFilter`** callback that is checked in three key places:
|
|
1. **Pool's `addRelay()`** ([line 21879](../www/ndk-worker.js:21879)) — refuses to add relays that fail the filter (including temporary outbox relays)
|
|
2. **Outbox tracker relay list resolution** ([line 54404](../www/ndk-worker.js:54404)) — filters disabled relays out of `readRelays` and `writeRelays` sets
|
|
3. **All three NDK pool implementations** check the filter ([line 47128](../www/ndk-worker.js:47128), [line 73531](../www/ndk-worker.js:73531), [line 78145](../www/ndk-worker.js:78145))
|
|
|
|
The `relayConnectionFilter` is already used elsewhere in the codebase (the `ndk-store` module at [line 83105](../www/ndk-worker.js:83105) uses it for `blockedRelays`), but the **worker's main NDK instance** (created at [line 90998](../www/ndk-worker.js:90998)) does **not** currently set it.
|
|
|
|
**This is the ideal mechanism for the disable feature** — setting `ndk.relayConnectionFilter` on the worker's main NDK instance will automatically prevent disabled relays from being used by the outbox model, without any additional outbox-specific code.
|
|
|
|
## Design
|
|
|
|
### Worker-side: `disabledRelays` Set
|
|
Add a `Set<string>` in the worker that tracks temporarily disabled relay URLs (normalized, trailing-slash form).
|
|
|
|
### `ndk.relayConnectionFilter` — the key mechanism
|
|
Set `ndk.relayConnectionFilter` on the worker's main NDK instance to check `disabledRelays`:
|
|
```js
|
|
ndk.relayConnectionFilter = (relayUrl) => {
|
|
return !disabledRelays.has(normalizeRelayUrl(relayUrl));
|
|
};
|
|
```
|
|
This automatically covers the outbox model:
|
|
- ✅ Prevents disabled relays from being added as temporary outbox relays
|
|
- ✅ Filters disabled relays out of outbox tracker's read/write relay sets
|
|
- ✅ Prevents NDK from connecting to disabled relays via any path
|
|
|
|
### New worker message handlers
|
|
|
|
#### `disableRelay` — disable a relay
|
|
1. Add the relay URL (normalized) to `disabledRelays`
|
|
2. Update `ndk.relayConnectionFilter` (or it reads `disabledRelays` live)
|
|
3. Get the relay from the pool, disconnect it
|
|
4. **Suppress auto-reconnect**: the NDK connectivity layer's `handleReconnection()` is called on disconnect. We need to intercept this.
|
|
|
|
#### `enableRelay` — re-enable a relay
|
|
1. Remove the relay URL from `disabledRelays`
|
|
2. Get the relay from the pool, call `relay.connect()`
|
|
3. If the relay isn't in the pool (e.g. write-only), add it on demand (same as `handleReconnectRelay` does)
|
|
|
|
#### `getDisabledRelays` — query disabled state
|
|
Returns the current `disabledRelays` set to the page, so the UI can show disabled state.
|
|
|
|
### Suppressing auto-reconnect
|
|
The NDK pool's `disconnectHandler` (see [line 21908](../www/ndk-worker.js:21908)) fires on relay disconnect and triggers reconnection. We need to prevent this for disabled relays.
|
|
|
|
**Approach**: In the worker's existing relay disconnect event handling, check if the relay URL is in `disabledRelays` before allowing reconnection. The disconnect handler in the pool calls `relay.connect()` on disconnect — we intercept by checking `disabledRelays` before calling connect.
|
|
|
|
The most surgical approach: in `attachRelayEventListeners` (where disconnect handlers are registered), add a check: if the relay URL is in `disabledRelays`, don't trigger reconnection. Since the worker wraps NDK, we can intercept at the worker level.
|
|
|
|
### `handleGetRelayData` enhancement
|
|
Add a `disabled` boolean field to each relay entry in the response, so pages can show disabled state without a separate query.
|
|
|
|
### `init-ndk.mjs` API
|
|
Add three new exported functions:
|
|
- `disableRelay(relayUrl)` — sends `disableRelay` message to worker
|
|
- `enableRelay(relayUrl)` — sends `enableRelay` message to worker
|
|
- `getDisabledRelays()` — sends `getDisabledRelays` message, returns Promise<Set>
|
|
|
|
### `relays.html` UI changes
|
|
- **Add a new "Enabled" column** between "Relay" and "Connected" in the relay table
|
|
- Uses the same checkbox style as the Read/Write/DM Inbox columns (`SVG_CHECKED`/`SVG_UNCHECKED`)
|
|
- Clicking the Enabled checkbox:
|
|
- If **enabled** (checked) → **disable** the relay (call `disableRelay()`, unchecks box, disconnects relay)
|
|
- If **disabled** (unchecked) → **enable** the relay (call `enableRelay()`, checks box, reconnects relay)
|
|
- The "Connected" column keeps its existing behavior (click to reconnect a disconnected relay)
|
|
- The `disabled` field from `getRelayData` drives the checkbox state
|
|
- The add-relay row gets a `-` placeholder in the Enabled column (new relays are enabled by default)
|
|
|
|
Current table columns:
|
|
```
|
|
| (remove) | Relay | Connected | Read | Write | DM Inbox | Reads | Writes | Connection Time |
|
|
```
|
|
|
|
New table columns:
|
|
```
|
|
| (remove) | Relay | Enabled | Connected | Read | Write | DM Inbox | Reads | Writes | Connection Time |
|
|
```
|
|
|
|
### Persistence
|
|
**No persistence** — disabled relays are session-only. On page reload / worker restart, all relays reconnect normally from `kind 10002`. This matches the "temporarily" requirement.
|
|
|
|
## Architecture Diagram
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
A[User clicks relay status icon] --> B{Relay state?}
|
|
B -- connected --> C[disableRelay - disconnect and suppress reconnect]
|
|
B -- disabled --> D[enableRelay - reconnect]
|
|
B -- disconnected, not disabled --> E[reconnectRelay - existing toggle]
|
|
|
|
C --> F[Worker: add to disabledRelays Set]
|
|
F --> G[Worker: relay.disconnect]
|
|
G --> H[NDK disconnect handler fires]
|
|
H --> I{Is relay in disabledRelays?}
|
|
I -- yes --> J[Skip auto-reconnect]
|
|
I -- no --> K[Auto-reconnect with backoff]
|
|
|
|
D --> L[Worker: remove from disabledRelays Set]
|
|
L --> M[Worker: relay.connect]
|
|
M --> N[Relay reconnects]
|
|
|
|
E --> O[Worker: disconnect + 500ms + reconnect]
|
|
```
|
|
|
|
## Outbox Model Toggle
|
|
|
|
Add a checkbox in the sidenav (alongside "Show connection history") to enable/disable the outbox model. Default: enabled.
|
|
|
|
### How it works
|
|
NDK has `autoConnectUserRelays` (default `true`) which controls whether NDK auto-connects to relays discovered via the outbox tracker. Toggling this at runtime effectively enables/disables the outbox model:
|
|
- **Disable**: set `ndk.autoConnectUserRelays = false`, disconnect all temporary/discovered relays
|
|
- **Enable**: set `ndk.autoConnectUserRelays = true`, let NDK re-discover and connect as needed
|
|
|
|
The `outboxTracker` still resolves relay lists when disabled, but NDK won't connect to those relays — so no outbox fetches happen.
|
|
|
|
### Persistence
|
|
`localStorage` key `outboxModel` (`'true'`/`'false'`), default `'true'`. Read on page load, sent to worker on init.
|
|
|
|
### UI
|
|
Same pattern as "Show connection history" checkbox — a clickable row in the sidenav with `SVG_CHECKED`/`SVG_UNCHECKED` icon.
|
|
|
|
## Implementation Steps
|
|
|
|
### Worker ([`www/ndk-worker.js`](../www/ndk-worker.js:1))
|
|
1. **Add `disabledRelays` Set** — near the top of the worker logic, alongside other relay state
|
|
2. **Set `ndk.relayConnectionFilter`** — after NDK init, set it to check `disabledRelays` (covers outbox model for disabled relays)
|
|
3. **Add `handleDisableRelay(relayUrl, port)`** — add to set, disconnect relay, suppress reconnect
|
|
4. **Add `handleEnableRelay(relayUrl, port)`** — remove from set, reconnect relay
|
|
5. **Add `handleGetDisabledRelays(requestId, port)`** — return the set
|
|
6. **Modify auto-reconnect suppression** — in the disconnect event handler or `attachRelayEventListeners`, check `disabledRelays` before allowing reconnection
|
|
7. **Modify `handleGetRelayData`** — add `disabled: boolean` field to each relay entry
|
|
8. **Add `handleSetOutboxModel(enabled, port)`** — set `ndk.autoConnectUserRelays`, disconnect temporary relays if disabling
|
|
9. **Add message handler cases** — `disableRelay`, `enableRelay`, `getDisabledRelays`, `setOutboxModel` in the switch statement
|
|
|
|
### Init module ([`www/js/init-ndk.mjs`](../www/js/init-ndk.mjs:1))
|
|
10. **Add `disableRelay(relayUrl)`** export — sends `disableRelay` message
|
|
11. **Add `enableRelay(relayUrl)`** export — sends `enableRelay` message
|
|
12. **Add `getDisabledRelays()`** export — sends `getDisabledRelays`, returns Promise<Set>
|
|
13. **Add `setOutboxModel(enabled)`** export — sends `setOutboxModel` message
|
|
|
|
### Relays page ([`www/relays.html`](../www/relays.html:1))
|
|
14. **Import new functions** — add `disableRelay`, `enableRelay`, `setOutboxModel` to the import from init-ndk.mjs
|
|
15. **Add "Enabled" column to table header** — new `<th>` between "Relay" and "Connected"
|
|
16. **Add "Enabled" checkbox to each relay row** — `SVG_CHECKED`/`SVG_UNCHECKED` based on `relay.disabled` field, with `data-enable-relay-url` attribute for click handler
|
|
17. **Add "Enabled" placeholder to add-relay row** — `-` (new relays are enabled by default)
|
|
18. **Add click handler for Enabled checkbox** — calls `disableRelay()` or `enableRelay()` based on current state, then refreshes relay data
|
|
19. **Keep existing "Connected" click handler** — still calls `handleRelayReconnect()` for reconnecting disconnected (but enabled) relays
|
|
20. **Add outbox model checkbox to sidenav** — alongside "Show connection history", with localStorage persistence and `setOutboxModel()` call
|
|
21. **Sync outbox model state on init** — read from localStorage, send to worker on page load
|
|
|
|
## Key Considerations
|
|
|
|
- **No `kind 10002` changes** — disabled relays stay in the user's relay list, they're just temporarily disconnected
|
|
- **Session-only** — disabled state is lost on worker restart / page reload
|
|
- **App-wide** — the worker is a SharedWorker, so disabling a relay on `relays.html` affects all open pages
|
|
- **Auto-reconnect suppression** — the critical piece; NDK will try to reconnect unless we explicitly prevent it
|
|
- **Write-only relays** — need to handle the case where a disabled relay isn't in the main pool (same as `handleReconnectRelay` does)
|
|
- **Outbox model coverage** — `ndk.relayConnectionFilter` automatically prevents disabled relays from being used by the outbox tracker, so disabled relays won't be added as temporary relays for fetching events from followed authors
|
|
- **`relayConnectionFilter` is already proven** — the `ndk-store` module already uses this exact pattern for `blockedRelays` at [line 83105](../www/ndk-worker.js:83105)
|