253 lines
9.9 KiB
Markdown
253 lines
9.9 KiB
Markdown
# Keep-Alive Page Architecture
|
||
|
||
## Overview
|
||
|
||
The keep-alive page is a utility that ensures a user's Nostr events persist across the ecosystem by discovering free relays and republishing events to them. It operates as a manual, step-by-step process with buttons for each phase, allowing the user to observe and control each stage.
|
||
|
||
## Core Concepts
|
||
|
||
### Event Priority System
|
||
|
||
Not all events are equally important. The page prioritizes event kinds for spreading:
|
||
|
||
| Priority | Kinds | Rationale |
|
||
|----------|-------|-----------|
|
||
| **Critical** | 7375, 7376, 17375, 37375 (Cashu wallet/token events), 10002 (relay list), 0 (profile metadata) | Loss = lost ecash or identity |
|
||
| **High** | 3 (contacts/follows), 10000 (mute list), 30078 (app-specific data), 30023 (long-form content) | Important user data |
|
||
| **Medium** | 30311 (live events), 30024 (drafts), 1063 (file metadata), 9735 (zap receipts) | Valuable but recoverable |
|
||
| **Low** | 1 (text notes), 6 (reposts), 7 (reactions), 1111 (comments) | High volume, low individual value |
|
||
|
||
### Relay Registry - kind 30078 with t-tag grouping
|
||
|
||
Discovered and tested relays are stored as addressable events using a `t` tag for grouping and sequential `d` tags for pagination. This keeps all data in Nostr events (portable across devices and apps) without needing a manifest.
|
||
|
||
**Event structure** (each chunk):
|
||
```json
|
||
{
|
||
"kind": 30078,
|
||
"tags": [
|
||
["d", "relay-list-001"],
|
||
["t", "relay-list"]
|
||
],
|
||
"content": "[{\"url\":\"wss://relay1.com\",\"available\":true,...}, ...]"
|
||
}
|
||
```
|
||
|
||
Each relay entry in the JSON content array:
|
||
```json
|
||
{
|
||
"url": "wss://relay.example.com",
|
||
"tested": 1711440000,
|
||
"available": true,
|
||
"nip11": { "name": "...", "description": "...", "supported_nips": [], "limitation": {} },
|
||
"geo": null
|
||
}
|
||
```
|
||
|
||
**Fetching**: A single filter retrieves all chunks:
|
||
```javascript
|
||
{ kinds: [30078], authors: [pubkey], "#t": ["relay-list"] }
|
||
```
|
||
|
||
**Pagination**: Each chunk holds ~100 relays (~50KB). Sequential `d` tags (`relay-list-001`, `002`, etc.) make each chunk independently replaceable.
|
||
|
||
**Republishing**: When re-testing, rebuild chunks and publish. If fewer chunks are needed than before, publish kind 5 deletion events for the extras.
|
||
|
||
**Size math**: ~500 bytes per relay with NIP-11 data × 100 relays = ~50KB per chunk. 300 relays = 3 events. 500 relays = 5 events. Well within relay limits.
|
||
|
||
**Cross-app usage**: Any app can discover the relay registry by querying for kind 30078 with `#t: relay-list` for a given pubkey.
|
||
|
||
## Architecture
|
||
|
||
### Data Flow
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
A[Step 1: Find My Events] --> B[Step 2: Find Relays]
|
||
B --> C[Step 3: Test Relays]
|
||
C --> D[Step 4: Store Relay Registry]
|
||
D --> E[Step 5: Spread Events]
|
||
|
||
A -->|Fetch all user events via NDK| A1[Events grouped by kind + priority]
|
||
B -->|Fetch kind 3 follows list| B1[Get follows pubkeys]
|
||
B1 -->|Fetch kind 10002 for each follow| B2[Extract unique relay URLs]
|
||
C -->|Generate session dummy nsec| C1[Post kind 1 test event to each relay]
|
||
C1 -->|Track success/failure| C2[Filtered list of available relays]
|
||
D -->|Publish kind 30078| D1[Relay registry persisted]
|
||
E -->|For each priority tier| E1[Publish events to available relays]
|
||
E1 -->|Rate limited - one relay at a time| E2[Progress log with success/fail per event]
|
||
```
|
||
|
||
### Page Layout
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
subgraph Header
|
||
H[KEEP ALIVE title]
|
||
end
|
||
|
||
subgraph Body
|
||
BTN[Step Buttons Row]
|
||
STATS[Stats Summary Panel]
|
||
LOG[Live Activity Log - scrollable]
|
||
end
|
||
|
||
subgraph Footer
|
||
F[Relay status + balance]
|
||
end
|
||
```
|
||
|
||
### Step Buttons
|
||
|
||
Each button represents a phase. They are enabled sequentially - Step 2 only enables after Step 1 completes, etc. Each button shows a status indicator:
|
||
|
||
- ○ Not started
|
||
- ◐ In progress
|
||
- ● Complete
|
||
- ✕ Error
|
||
|
||
Buttons:
|
||
1. **Find My Events** - Fetches all user events, displays count by kind
|
||
2. **Find Relays** - Discovers relays from follows' kind 10002
|
||
3. **Test Relays** - Tests each discovered relay with dummy event
|
||
4. **Save Registry** - Stores tested relay data as kind 30078
|
||
5. **Spread: Critical** - Spreads critical priority events
|
||
6. **Spread: High** - Spreads high priority events
|
||
7. **Spread: Medium** - Spreads medium priority events
|
||
8. **Spread: Low** - Spreads low priority events
|
||
|
||
### Stats Summary Panel
|
||
|
||
Shows at-a-glance numbers:
|
||
- Total events found: X
|
||
- Events by priority: Critical: X | High: X | Medium: X | Low: X
|
||
- Relays discovered: X
|
||
- Relays available: X / X tested
|
||
- Events spread: X / X total
|
||
|
||
### Live Activity Log
|
||
|
||
A scrollable log area showing real-time progress messages:
|
||
```
|
||
[12:34:01] Fetching user events...
|
||
[12:34:03] Found 142 events across 8 kinds
|
||
[12:34:03] kind 1: 89 events (low priority)
|
||
[12:34:03] kind 0: 1 event (critical priority)
|
||
[12:34:03] kind 7375: 3 events (critical priority)
|
||
...
|
||
[12:35:10] Testing wss://relay.damus.io ... ✓ available
|
||
[12:35:12] Testing wss://relay.nostr.band ... ✓ available
|
||
[12:35:15] Testing wss://broken.relay.xyz ... ✕ timeout
|
||
```
|
||
|
||
## Implementation Details
|
||
|
||
### Phase 1: Page Skeleton
|
||
|
||
- Modify `www/keep-alive.html` from template
|
||
- Title: "KEEP ALIVE"
|
||
- CSS for button row, stats panel, log area
|
||
- Import required NDK functions: `initNDKPage`, `getPubkey`, `subscribe`, `publishEvent`, `ndkFetchEvents`, `fetchEvents`, `getVersion`, etc.
|
||
- All page logic inline in the HTML script module, following existing page patterns
|
||
|
||
### Phase 2: Find My Events
|
||
|
||
```javascript
|
||
// Fetch all events authored by the user
|
||
const filters = { authors: [currentPubkey] };
|
||
const events = await ndkFetchEvents(filters);
|
||
|
||
// Group by kind and assign priority
|
||
const eventsByKind = new Map();
|
||
for (const event of events) {
|
||
if (!eventsByKind.has(event.kind)) eventsByKind.set(event.kind, []);
|
||
eventsByKind.get(event.kind).push(event);
|
||
}
|
||
```
|
||
|
||
Priority assignment uses the table above. Events are stored in memory for the spreading phase.
|
||
|
||
### Phase 3: Find Relays
|
||
|
||
1. Fetch user's kind 3 event to get follows list
|
||
2. Extract pubkeys from `p` tags
|
||
3. Fetch kind 10002 events for all follows: `{ kinds: [10002], authors: [followPubkeys...] }`
|
||
4. Parse `r` tags from each 10002 event to extract relay URLs
|
||
5. Deduplicate and normalize URLs
|
||
6. Also check if we already have a stored relay registry (kind 30078 with `#t: relay-list`) and merge
|
||
|
||
### Phase 4: Test Relays
|
||
|
||
Uses the user's own kind 0 (profile metadata) event as the test payload. This is ideal because:
|
||
- It's already signed by the user — no dummy keypair needed
|
||
- Every relay accepts kind 0 events
|
||
- It serves double duty: testing the relay AND spreading the user's profile
|
||
|
||
For each discovered relay:
|
||
1. Use the user's kind 0 event fetched in Phase 2 (already signed)
|
||
2. Open a direct WebSocket connection to the relay (bypassing NDK pool since these are not user relays)
|
||
3. Send the kind 0 event as the test
|
||
4. Wait for OK response or timeout (5 seconds)
|
||
5. Also fetch NIP-11 info document via HTTP: `fetch(relayUrl.replace('wss://', 'https://'), { headers: { Accept: 'application/nostr+json' } })`
|
||
6. Record result: available/unavailable, NIP-11 data, response time
|
||
|
||
**Important**: This uses raw WebSocket connections, not the NDK worker, because we don't want to pollute the user's relay pool with hundreds of test relays.
|
||
|
||
### Phase 5: Store Relay Registry
|
||
|
||
1. Build relay metadata array from test results
|
||
2. Chunk into events of ~100 relays each with `d: "relay-list-001"`, `d: "relay-list-002"`, etc.
|
||
3. Each event tagged with `["t", "relay-list"]`
|
||
4. Publish via `publishEvent()` (uses user's connected relays)
|
||
5. If fewer chunks needed than previously stored, publish kind 5 deletion events for extras
|
||
|
||
### Phase 6: Spread Events
|
||
|
||
For each priority tier, when the user clicks the corresponding spread button:
|
||
|
||
1. Iterate through available relays
|
||
2. For each relay, open a WebSocket connection
|
||
3. Send events one at a time with a delay between each (rate limiting)
|
||
4. Use `publishRawEvent`-style approach: events are already signed by the user
|
||
5. Track success/failure per event per relay
|
||
6. Log progress in real-time
|
||
7. Rate limiting: ~500ms between events to the same relay, process one relay at a time
|
||
|
||
**Relay kindness rules**:
|
||
- Respect NIP-11 `limitation.max_event_tags`, `limitation.max_content_length`
|
||
- Don't send events that exceed relay's stated limits
|
||
- If relay returns `blocked:` or `rate-limited:` in OK response, back off and skip
|
||
- Close WebSocket cleanly after batch is done
|
||
|
||
### Worker Considerations
|
||
|
||
The keep-alive page will need to do direct WebSocket operations for:
|
||
- Testing relays with dummy events (Phase 4)
|
||
- Spreading events to non-pool relays (Phase 6)
|
||
|
||
This will be done **client-side in the page script**, not through the NDK worker, because:
|
||
- We don't want to add hundreds of relays to the NDK pool
|
||
- We need fine-grained control over individual relay connections
|
||
- The worker's publish function only publishes to pool relays
|
||
|
||
The NDK worker is still used for:
|
||
- Authentication and user pubkey
|
||
- Fetching user events (Phase 2)
|
||
- Fetching follows and kind 10002 events (Phase 3)
|
||
- Publishing the relay registry kind 30078 (Phase 5)
|
||
|
||
## File Structure
|
||
|
||
All implementation is in a single file following existing patterns:
|
||
- `www/keep-alive.html` - Complete page with inline styles and script module
|
||
|
||
No new JS modules needed for Phase 1. If the WebSocket relay testing/spreading logic grows complex, it could be extracted to `www/js/keep-alive.mjs` later.
|
||
|
||
## Future Enhancements
|
||
|
||
- **Geo-location**: Resolve relay hostnames to IP addresses, use a geo-IP service, store lat/lng in relay registry metadata. Display on a world map showing where user data exists.
|
||
- **Broader discovery**: Fetch kind 10002 from popular relays for random pubkeys to discover more relays beyond the user's social graph.
|
||
- **Automation**: Add a "Run All" button that executes all steps sequentially.
|
||
- **Scheduling**: Option to run keep-alive periodically (e.g., weekly) using a service worker or reminder.
|
||
- **Diff mode**: Show which events are already on which relays before spreading, to avoid redundant publishes.
|