Files
client/README.md
2026-04-17 16:52:51 -04:00

183 lines
9.4 KiB
Markdown

# client
A nostr web application framework built on [NDK (Nostr Development Kit)](https://github.com/nostr-dev-kit/ndk). Uses a SharedWorker architecture where a single NDK instance manages all relay connections, caching, and subscriptions across multiple browser tabs.
## Architecture Overview
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ index.html │ │ post.html │ │ cashu.html │ ... (any page)
│ init-ndk.mjs│ │ init-ndk.mjs│ │ init-ndk.mjs│
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ postMessage │ │
└─────────────┬────┘─────────────────┘
┌─────────────────┐
│ ndk-worker.js │ (SharedWorker — single instance)
│ ┌─────────────┐ │
│ │ NDK │ │
│ │ + Dexie │ │ ← IndexedDB cache (ndk-shared)
│ │ + Signer │ │
│ └──────┬──────┘ │
└─────────┼────────┘
┌────────────────────────┐
│ Nostr Relay Pool │
│ wss://relay1 relay2 │
└────────────────────────┘
```
## Event Management Strategy
This section defines **when and how** nostr events are fetched, cached, and subscribed to. It serves as the single source of truth for event lifecycle management.
### Cache Behavior
All events flowing through NDK are automatically cached in **IndexedDB via the Dexie adapter** (database: `ndk-shared`). This means:
- Any `ndk.fetchEvents()` call caches results automatically
- Any subscription event received is cached via `cacheAdapter.setEvent()`
- Pages can query the cache directly via `queryCache()` without hitting relays
- NDK's `CACHE_FIRST` mode returns cached data immediately, then updates from relays
### Startup Events (fetched by worker on init)
These events are fetched once during `handleInit()` when the first tab connects. They populate the Dexie cache and provide essential app-wide state.
| Priority | Kind | NIP | Name | Filter | Subscription | Purpose |
|----------|------|-----|------|--------|-------------|---------|
| 1 | `0` | NIP-01 | User Metadata | `authors:[pubkey]` | No — one-shot | Profile display (name, picture, about) |
| 2 | `10002` | NIP-65 | Relay List | `authors:[pubkey], limit:1` | **Yes** — persistent `closeOnEose:false` | Configure relay pool; live updates |
| 3 | `30078` | NIP-78 | App Settings | `authors:[pubkey], #d:[user-settings]` | No — one-shot | Encrypted user preferences (NIP-44) |
| 4 | `17375` | NIP-60 | Cashu Wallet | `authors:[pubkey]` | **Yes** — startup fetch + persistent | Wallet metadata, mint list, and live wallet changes |
| 5 | `7375` | NIP-60 | Cashu Tokens | `authors:[pubkey]` | **Yes** — startup fetch + persistent | Unspent proofs and live token changes |
| 6 | `5` | NIP-09 | Event Deletion | `authors:[pubkey], #k:[7375]` | **Yes** — startup fetch + persistent | Track spent/deleted tokens |
**Startup sequence (current implementation):**
```
handleInit(pubkey)
├── initNDK() // Create NDK, connect relays
├── hydrateUserSettingsForPubkey() // kind 30078
├── fetchUserProfile() // kind 0
├── fetchUserRelays() // kind 10002 (one-shot)
├── subscribe kind 10002 // persistent relay list updates
├── fetchStartupEventDownloads() // one-shot preload: kinds 3, 17375, 7375, 5(#k=7375)
└── ensureStartupWalletSubscriptions() // persistent wallet subscriptions: 17375, 7375, 5(#k=7375)
```
This means wallet information is not only prefetched at startup; it is now also continuously updated by long-lived worker subscriptions.
### On-Demand Events (fetched by individual pages)
These events are fetched when a specific page loads. They benefit from the Dexie cache — if the same events were previously fetched, they're served from cache first.
| Kind | NIP | Name | Pages | Pattern |
|------|-----|------|-------|---------|
| `0` | NIP-01 | User Metadata | profile, msg, post, npub | `fetchEventsFromAllRelays` or `subscribe` |
| `1` | NIP-01 | Short Text Notes | post, index, links, strudel, template | `subscribe` with `CACHE_FIRST` |
| `3` | NIP-02 | Contact List | post, profile, cashu | `subscribe` + `queryCache` + `fetchEventsFromAllRelays` |
| `4` | NIP-04 | Encrypted DMs | msg | `subscribe` with `CACHE_FIRST` |
| `1059` | NIP-44 | Gift Wrap (DMs) | msg | `subscribe` with `CACHE_FIRST` |
| `5` | NIP-09 | Event Deletion | (via startup sub) | Persistent subscription |
| `7` | NIP-25 | Reactions | post (via interactions) | `fetchEventsFromAllRelays` |
| `6` | NIP-18 | Reposts | post (via interactions) | `fetchEventsFromAllRelays` |
| `9735` | NIP-57 | Zap Receipts | post (via interactions) | `fetchEventsFromAllRelays` |
| `10019` | NIP-60 | Mint List | cashu (mint discovery) | `fetchEventsFromAllRelays` |
| `10063` | NIP-96 | Blossom Server List | blobs | `subscribe` with `CACHE_FIRST` |
| `17375` | NIP-60 | Cashu Wallet | cashu (full wallet ops) | Via NDKCashuWallet |
| `7375` | NIP-60 | Cashu Tokens | cashu (full wallet ops) | Via NDKCashuWallet (cache-first) |
| `7376` | NIP-60 | Spending History | cashu (transaction list) | Via NDKCashuWallet |
| `30023` | NIP-23 | Long-form Content | note | `subscribe` with `CACHE_FIRST` |
| `30024` | NIP-23 | Draft Long-form | note | `subscribe` with `CACHE_FIRST` |
| `30078` | NIP-78 | App-specific Data | todo, cal, links, post (viewed) | `subscribe` or `fetchEvents` |
| `38000` | NIP-89 | Recommendations | cashu (mint discovery) | `fetchEventsFromAllRelays` |
| `38421` | — | AI Service Announcements | ai | `subscribe` with `CACHE_FIRST` |
### Subscription Patterns
The app uses three distinct patterns for fetching events:
#### 1. One-shot Fetch
```javascript
// Fetch once, cache result, done
const events = await ndk.fetchEvents(filter);
```
Used for: startup profile, relay list, wallet definition, user settings
#### 2. Subscribe with CACHE_FIRST
```javascript
// Returns cached events immediately, then live events from relays
subscribe(filter, { closeOnEose: false, cacheUsage: 'CACHE_FIRST' });
```
Used for: most page-level subscriptions (posts, contacts, notes, DMs)
#### 3. Persistent Worker Subscription
```javascript
// Long-lived subscription in the worker, survives page navigation
ndk.subscribe(filter, { closeOnEose: false });
```
Used for: relay list updates (kind 10002), wallet metadata (kind 17375), token events (kind 7375), and token deletions (kind 5 with #k=7375)
### Cache-First Advantage
Because startup events are fetched early and cached in Dexie, subsequent page loads benefit:
- **post.html** subscribes to kind 3 (contacts) → if profile.html already fetched it, cache hit
- **cashu.html** starts NDKCashuWallet → kind 7375 tokens already in cache from startup → instant optimistic balance
- **Any page** fetching kind 0 (profile) → already cached from startup
### Modifying Event Timing
To change when an event kind is fetched:
1. **Move to startup:** Add the fetch to `fetchStartupWalletState()` or create a new startup function in `handleInit()`. This pre-populates the cache for all pages.
2. **Move to on-demand:** Remove from startup, let individual pages fetch as needed. First load will be slower but startup will be faster.
3. **Add a subscription:** For events that change frequently, add a persistent subscription in the worker. This keeps the cache fresh without page-level polling.
**Trade-offs:**
- More startup events = slower initial load, faster page navigation
- Fewer startup events = faster initial load, slower first page that needs the data
- Persistent subscriptions = always fresh data, more relay bandwidth
## Project Structure
```
www/
├── ndk-worker.js # SharedWorker — NDK instance, cache, subscriptions
├── js/
│ ├── init-ndk.mjs # Client-side NDK API (shared by all pages)
│ ├── relay-ui.mjs # Footer relay status + balance display
│ ├── cashu-wallet.mjs # Cashu wallet controller (cashu.html only)
│ ├── post-interactions.mjs # Social interactions (reactions, reposts, zaps)
│ └── utilities.mjs # Shared utilities
├── css/client.css # Shared styles
├── *.html # Individual pages
└── ndk-core.bundle.js # Bundled NDK library
build/
├── ndk-entry.js # Bundle entry point
└── ndk-core.bundle.js # Built bundle
ndk/ # NDK source (submodule/clone)
├── core/ # Core NDK library
├── wallet/ # Cashu wallet implementation
├── cache-browser/ # Browser cache adapter
├── cache-dexie/ # Dexie (IndexedDB) cache
└── ...
plans/ # Architecture and implementation plans
```
## Building
```bash
# Build the NDK bundle
node build-ndk-bundle.js
# Serve locally
python serve_local.py
```