183 lines
9.4 KiB
Markdown
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
|
|
```
|