970 lines
29 KiB
Markdown
970 lines
29 KiB
Markdown
# NDK Migration Plan: Distributed Web Apps with Shared NDK Worker
|
|
|
|
## Executive Summary
|
|
|
|
This plan outlines the migration of the existing custom Nostr client implementation (`client/`) to use NDK (Nostr Development Kit) with Dexie cache. The migration will modernize the codebase, reduce maintenance burden, and leverage NDK's built-in features for relay management, subscriptions, and caching.
|
|
|
|
**Primary Goal**: Create independently distributable web pages in `www/` that share a common NDK SharedWorker
|
|
|
|
**Key Architectural Requirements**:
|
|
1. Each page must be self-contained and independently accessible
|
|
2. All pages will be created in `www/` directory (using existing `upload_www.sh`)
|
|
3. **Preserve all aesthetics from `client/`** - same layouts, CSS, page formatting
|
|
4. Users should not notice backend changes - only improved performance/reliability
|
|
|
|
**Technical Requirements**:
|
|
- Users can visit any page directly (e.g., `/www/profile.html`)
|
|
- Each page checks authentication and launches nostr-login-lite if needed
|
|
- All pages share the same NDK SharedWorker for relay management
|
|
- Pages can be distributed individually or as a complete suite
|
|
- No duplication of relay connections when multiple pages are open
|
|
|
|
---
|
|
|
|
## Architecture Comparison
|
|
|
|
### Current Custom Implementation
|
|
|
|
```mermaid
|
|
graph TB
|
|
A[client/index.html] --> B[nostr-login-lite]
|
|
A --> C[client/js/init.mjs]
|
|
C --> D[client/shared_worker.mjs]
|
|
D --> E[Custom WebSocket Management]
|
|
D --> F[client/js/db.js - Custom Dexie Schema]
|
|
A --> G[liveQuery Observables]
|
|
G --> F
|
|
E --> H[Manual Relay Connections]
|
|
E --> I[Manual Subscription Management]
|
|
D --> J[Message-based Signing via window.nostr]
|
|
```
|
|
|
|
**Key Components:**
|
|
- **Authentication**: nostr-login-lite → window.nostr facade
|
|
- **SharedWorker**: Custom implementation with manual relay/subscription management
|
|
- **Database**: Custom Dexie schema (DB_NOSTR) with specific event tables
|
|
- **Relay Management**: Manual WebSocket connections, reconnection logic, exponential backoff
|
|
- **Subscriptions**: Custom subscription queue system with authentication-aware processing
|
|
- **Data Flow**: SharedWorker updates DB → liveQuery observables → UI updates
|
|
|
|
### Target NDK Implementation
|
|
|
|
```mermaid
|
|
graph TB
|
|
A[Any Page: index.html, profile.html, feed.html, etc.] --> B[nostr-login-lite]
|
|
A --> C[js/init-ndk.mjs - Shared Init Module]
|
|
C --> D[ndk_worker.mjs - Shared NDK Worker]
|
|
D --> E[NDK Instance Singleton]
|
|
E --> F[NDKPool - Relay Management]
|
|
E --> G[NDKCacheBrowser with Dexie Adapter]
|
|
A --> H[NDK Event Subscriptions]
|
|
H --> I[Event Handlers]
|
|
F --> J[Automatic Relay Connections]
|
|
F --> K[Automatic Subscription Deduplication]
|
|
D --> L[Message-based Communication]
|
|
```
|
|
|
|
**Key Components:**
|
|
- **Authentication**: nostr-login-lite → window.nostr facade (unchanged)
|
|
- **SharedWorker**: NDK-based worker shared by ALL pages
|
|
- **Database**: NDK's Dexie cache adapter (`ndk-client`) shared by ALL pages
|
|
- **Relay Management**: NDKPool handles connections, reconnection, health monitoring
|
|
- **Subscriptions**: NDK handles deduplication, caching, and relay selection
|
|
- **Data Flow**: NDK subscriptions → event handlers → UI updates
|
|
- **Independent Pages**: Each page can be accessed directly, authenticates if needed
|
|
|
|
---
|
|
|
|
## Key Differences Analysis
|
|
|
|
| Aspect | Custom Implementation | NDK Implementation | Migration Impact |
|
|
|--------|----------------------|-------------------|------------------|
|
|
| **Relay Connections** | Manual WebSocket management, custom reconnection logic | NDKPool with built-in connection management | Remove ~500 lines of WebSocket code |
|
|
| **Subscriptions** | Custom queue system, manual placeholder replacement | NDK subscriptions with automatic deduplication | Simplify subscription logic significantly |
|
|
| **Caching** | Custom Dexie schema per event kind | NDK standardized cache schema | Start with fresh NDK cache |
|
|
| **Event Storage** | Separate tables per kind (Event_0, Event_3, etc.) | Unified events table with indexes | Major schema change |
|
|
| **Authentication** | Custom message-based signing | Can use NDK signer or keep message-based | Keep existing pattern for compatibility |
|
|
| **Relay Selection** | Manual relay list from Event 10002 | NDK outbox model with automatic relay selection | Can leverage NDK's smarter relay selection |
|
|
| **Error Handling** | Custom error handling per operation | NDK built-in error handling and retry logic | Simplified error handling |
|
|
| **Page Independence** | Requires launcher page | Each page independently accessible | Major architectural improvement |
|
|
|
|
---
|
|
|
|
## Implementation Strategy
|
|
|
|
### Distributed Architecture with Shared Worker
|
|
|
|
Each page is self-contained and independently accessible, but all pages share the same NDK SharedWorker.
|
|
|
|
**Directory Structure:**
|
|
```
|
|
www/
|
|
├── index.html (launcher page with app grid - NDK version)
|
|
├── ndk-worker.js (shared NDK worker - already exists, enhance it)
|
|
├── ndk-core.bundle.js (NDK bundle - already exists)
|
|
├── profile.html (standalone profile page)
|
|
├── feed.html (standalone feed page)
|
|
├── msg.html (standalone messaging page)
|
|
├── post.html (standalone post composer)
|
|
├── relays.html (standalone relay manager)
|
|
├── db.html (standalone database viewer)
|
|
├── ... (other standalone pages)
|
|
├── css/
|
|
│ └── client.css (copied from client/css/client.css)
|
|
├── js/
|
|
│ └── (any additional shared JS modules)
|
|
└── (nostr-login-lite already available)
|
|
|
|
client/ (reference for aesthetics)
|
|
├── index.html (reference for layout/styling)
|
|
├── css/
|
|
│ └── client.css (source of truth for styling)
|
|
└── html/
|
|
└── (reference pages for layout/functionality)
|
|
```
|
|
|
|
**Key Principles:**
|
|
1. **Self-Contained Pages**: Each HTML page includes authentication check
|
|
2. **Shared Worker**: All pages connect to same `ndk-worker.js`
|
|
3. **Shared Cache**: All pages use same NDK Dexie database (`ndk-client`)
|
|
4. **Shared Authentication**: nostr-login-lite state shared across tabs
|
|
5. **Independent Distribution**: Each page can be distributed separately
|
|
6. **Preserve Aesthetics**: Copy CSS, layouts, and styling from `client/`
|
|
7. **Backend Only Changes**: Users see same UI, just better performance
|
|
|
|
**Benefits:**
|
|
- Users can bookmark/share specific tools (e.g., just the profile editor)
|
|
- No need to go through launcher to access functionality
|
|
- Shared worker prevents duplicate relay connections
|
|
- Shared cache means instant data availability across pages
|
|
- Modular architecture - easy to add/remove pages
|
|
- Flexible distribution options
|
|
- **Familiar UI**: Users see same interface they're used to
|
|
- **Easy deployment**: Use existing `upload_www.sh` script
|
|
|
|
---
|
|
|
|
## Detailed Implementation Plan
|
|
|
|
### Step 1: Enhance Existing NDK SharedWorker
|
|
|
|
**File**: `www/ndk-worker.js` (already exists, enhance it)
|
|
|
|
**Key Features:**
|
|
- Initialize NDK with Dexie cache adapter
|
|
- Handle authentication via message-based communication
|
|
- Manage NDK subscriptions
|
|
- Broadcast events to connected ports
|
|
- Handle relay list from Event 10002
|
|
|
|
**Architecture:**
|
|
```javascript
|
|
// Core structure
|
|
let ndk = null;
|
|
let cachedProfile = null;
|
|
let activePorts = new Set();
|
|
let activeSubscriptions = new Map();
|
|
|
|
// Initialize NDK with Dexie cache
|
|
async function initNDK(pubkey) {
|
|
const cacheAdapter = new NDKCacheBrowser({
|
|
dbName: 'ndk-client',
|
|
forceAdapter: 'dexie'
|
|
});
|
|
|
|
ndk = new NDK({
|
|
explicitRelayUrls: BOOTSTRAP_RELAYS,
|
|
cacheAdapter: cacheAdapter
|
|
});
|
|
|
|
await cacheAdapter.initializeAsync(ndk);
|
|
await ndk.connect();
|
|
}
|
|
|
|
// Handle subscriptions with NDK
|
|
async function createSubscription(filters, opts) {
|
|
const sub = ndk.subscribe(filters, opts);
|
|
|
|
sub.on('event', (event) => {
|
|
// Broadcast to all ports
|
|
broadcastToAllPorts({ type: 'event', data: event.rawEvent() });
|
|
});
|
|
|
|
sub.on('eose', () => {
|
|
broadcastToAllPorts({ type: 'eose' });
|
|
});
|
|
|
|
return sub;
|
|
}
|
|
```
|
|
|
|
### Step 2: Copy CSS and Create Shared Utilities
|
|
|
|
**Files to Copy:**
|
|
- `client/css/client.css` → `www/css/client.css`
|
|
- `client/css/*.ttf` → `www/css/*.ttf` (fonts)
|
|
|
|
**Create**: `www/js/init-ndk.mjs` (new shared initialization module)
|
|
|
|
**Key Features:**
|
|
- Check if user is already authenticated
|
|
- Launch nostr-login-lite if not authenticated
|
|
- Initialize NDK worker connection (shared across all pages)
|
|
- Handle authentication with nostr-login-lite
|
|
- Set up event listeners for worker messages
|
|
- Provide helper functions for subscriptions
|
|
- **Path-aware worker loading** - works from any page location
|
|
|
|
**Architecture:**
|
|
```javascript
|
|
let ndkWorker = null;
|
|
|
|
/**
|
|
* Initialize NDK for any page - handles authentication automatically
|
|
* This function can be called from any page (index.html, profile.html, etc.)
|
|
*/
|
|
export async function initNDKPage() {
|
|
console.log('Initializing NDK page...');
|
|
|
|
// Check if already authenticated
|
|
const authenticated = await ensureAuthenticated();
|
|
if (!authenticated) {
|
|
console.log('Not authenticated - launching nostr-login-lite');
|
|
throw new Error("Authentication required");
|
|
}
|
|
|
|
console.log('User authenticated, initializing NDK worker');
|
|
|
|
// All pages in www/ directory, so worker path is always the same
|
|
const workerPath = './ndk-worker.js';
|
|
|
|
// Initialize NDK SharedWorker (shared across all tabs/pages)
|
|
ndkWorker = new SharedWorker(workerPath, { type: 'module' });
|
|
|
|
// Send authentication to worker
|
|
const pubkey = await window.nostr.getPublicKey();
|
|
ndkWorker.port.postMessage({
|
|
type: 'init',
|
|
pubkey: pubkey
|
|
});
|
|
|
|
// Set up message handlers
|
|
ndkWorker.port.onmessage = handleWorkerMessage;
|
|
|
|
console.log('NDK worker initialized and connected');
|
|
|
|
return { authenticated: true, worker: ndkWorker };
|
|
}
|
|
|
|
/**
|
|
* Ensure user is authenticated - works from any page
|
|
* If not authenticated, launches nostr-login-lite modal
|
|
*/
|
|
async function ensureAuthenticated() {
|
|
// Check if window.nostr exists (from nostr-login-lite or extension)
|
|
if (window.nostr) {
|
|
try {
|
|
await window.nostr.getPublicKey();
|
|
return true;
|
|
} catch (error) {
|
|
console.log('window.nostr exists but not authenticated');
|
|
}
|
|
}
|
|
|
|
// Launch nostr-login-lite
|
|
if (window.NOSTR_LOGIN_LITE) {
|
|
await window.NOSTR_LOGIN_LITE.launch();
|
|
// Wait for authentication
|
|
return new Promise((resolve) => {
|
|
const checkAuth = async () => {
|
|
if (window.nostr) {
|
|
try {
|
|
await window.nostr.getPublicKey();
|
|
resolve(true);
|
|
} catch {
|
|
setTimeout(checkAuth, 100);
|
|
}
|
|
} else {
|
|
setTimeout(checkAuth, 100);
|
|
}
|
|
};
|
|
checkAuth();
|
|
});
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Helper to create subscriptions
|
|
export function subscribe(filters, opts = {}) {
|
|
return new Promise((resolve) => {
|
|
const subId = generateSubId();
|
|
|
|
ndkWorker.port.postMessage({
|
|
type: 'subscribe',
|
|
subId: subId,
|
|
filters: filters,
|
|
opts: opts
|
|
});
|
|
|
|
// Return subscription handle
|
|
resolve({
|
|
subId: subId,
|
|
close: () => {
|
|
ndkWorker.port.postMessage({
|
|
type: 'unsubscribe',
|
|
subId: subId
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Helper to get current user's pubkey
|
|
export async function getPubkey() {
|
|
if (!window.nostr) {
|
|
throw new Error('Not authenticated');
|
|
}
|
|
return await window.nostr.getPublicKey();
|
|
}
|
|
```
|
|
|
|
### Step 3: Create Standalone Page Template
|
|
|
|
**File**: `client/html/profile.html` (example standalone page)
|
|
|
|
**Key Features:**
|
|
- Self-contained - can be accessed directly
|
|
- Includes authentication check
|
|
- Connects to shared NDK worker
|
|
- Works independently or as part of suite
|
|
|
|
**Template Structure:**
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Profile Editor</title>
|
|
|
|
<!-- Shared styles -->
|
|
<link rel="stylesheet" href="../css/client.css">
|
|
|
|
<!-- nostr-login-lite -->
|
|
<script src="../nostr_login_lite/build/nostr-lite.js"></script>
|
|
</head>
|
|
<body>
|
|
<div id="app">
|
|
<!-- Page content here -->
|
|
</div>
|
|
|
|
<script type="module">
|
|
import { initNDKPage, subscribe, getPubkey } from '../js/init-ndk.mjs';
|
|
|
|
// Initialize page - handles authentication automatically
|
|
async function init() {
|
|
try {
|
|
// This will authenticate user if needed
|
|
await initNDKPage();
|
|
|
|
// Now we're authenticated and connected to NDK worker
|
|
const pubkey = await getPubkey();
|
|
|
|
// Load profile data
|
|
await loadProfile(pubkey);
|
|
|
|
} catch (error) {
|
|
console.error('Initialization failed:', error);
|
|
}
|
|
}
|
|
|
|
async function loadProfile(pubkey) {
|
|
// Subscribe to profile (kind 0)
|
|
const sub = await subscribe(
|
|
{ kinds: [0], authors: [pubkey], limit: 1 },
|
|
{ closeOnEose: false, cacheUsage: 'CACHE_FIRST' }
|
|
);
|
|
|
|
// Handle profile updates
|
|
window.addEventListener('ndkEvent', (event) => {
|
|
if (event.detail.kind === 0 && event.detail.pubkey === pubkey) {
|
|
updateProfileUI(event.detail);
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateProfileUI(profile) {
|
|
// Update UI with profile data
|
|
const content = JSON.parse(profile.content);
|
|
document.getElementById('name').value = content.name || '';
|
|
document.getElementById('about').value = content.about || '';
|
|
// etc.
|
|
}
|
|
|
|
// Start initialization
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
**Key Points:**
|
|
- Each page calls `initNDKPage()` which handles authentication
|
|
- If user not logged in, nostr-login-lite modal appears
|
|
- After authentication, page connects to shared NDK worker
|
|
- All pages share same worker = no duplicate relay connections
|
|
- All pages share same cache = instant data availability
|
|
|
|
### Step 4: Create Launcher Page (index.html)
|
|
|
|
**File**: `client/index.html`
|
|
|
|
**Purpose**: App launcher with grid of available tools
|
|
|
|
**Key Features:**
|
|
- Also self-contained (can be accessed directly)
|
|
- Shows grid of available apps/tools
|
|
- Each button links to standalone page
|
|
- Also uses shared NDK worker for any data it needs
|
|
|
|
**Structure:**
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Nostr Tools</title>
|
|
<link rel="stylesheet" href="./css/client.css">
|
|
<script src="./nostr_login_lite/build/nostr-lite.js"></script>
|
|
</head>
|
|
<body>
|
|
<div id="app-launcher">
|
|
<h1>Nostr Tools</h1>
|
|
|
|
<div class="app-grid">
|
|
<a href="./html/profile.html" class="app-button">
|
|
<div class="app-icon">👤</div>
|
|
<div class="app-name">Profile</div>
|
|
</a>
|
|
|
|
<a href="./html/feed.html" class="app-button">
|
|
<div class="app-icon">📰</div>
|
|
<div class="app-name">Feed</div>
|
|
</a>
|
|
|
|
<a href="./html/msg.html" class="app-button">
|
|
<div class="app-icon">💬</div>
|
|
<div class="app-name">Messages</div>
|
|
</a>
|
|
|
|
<a href="./html/post.html" class="app-button">
|
|
<div class="app-icon">✍️</div>
|
|
<div class="app-name">Post</div>
|
|
</a>
|
|
|
|
<!-- More app buttons... -->
|
|
</div>
|
|
</div>
|
|
|
|
<script type="module">
|
|
import { initNDKPage, getPubkey } from './js/init-ndk.mjs';
|
|
|
|
async function init() {
|
|
try {
|
|
// Authenticate and connect to NDK worker
|
|
await initNDKPage();
|
|
|
|
// Show user info in header
|
|
const pubkey = await getPubkey();
|
|
displayUserInfo(pubkey);
|
|
|
|
} catch (error) {
|
|
console.error('Initialization failed:', error);
|
|
}
|
|
}
|
|
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
### Step 5: Implement Core Features (Shared Across Pages)
|
|
|
|
#### A. Profile Loading
|
|
```javascript
|
|
// Subscribe to user profile (kind 0)
|
|
async function loadUserProfile(pubkey) {
|
|
const sub = await subscribe(
|
|
{ kinds: [0], authors: [pubkey], limit: 1 },
|
|
{ closeOnEose: false, cacheUsage: 'CACHE_FIRST' }
|
|
);
|
|
|
|
// NDK will emit cached profile immediately, then update from relays
|
|
}
|
|
```
|
|
|
|
#### B. Relay List Management (Event 10002)
|
|
```javascript
|
|
// Subscribe to relay list
|
|
async function loadRelayList(pubkey) {
|
|
const sub = await subscribe(
|
|
{ kinds: [10002], authors: [pubkey], limit: 1 },
|
|
{ closeOnEose: false, cacheUsage: 'CACHE_FIRST' }
|
|
);
|
|
|
|
// Worker will automatically update NDK relay pool when Event 10002 received
|
|
}
|
|
```
|
|
|
|
#### C. Contacts/Follows (Event 3)
|
|
```javascript
|
|
// Subscribe to contacts
|
|
async function loadContacts(pubkey) {
|
|
const sub = await subscribe(
|
|
{ kinds: [3], authors: [pubkey], limit: 1 },
|
|
{ closeOnEose: false, cacheUsage: 'CACHE_FIRST' }
|
|
);
|
|
}
|
|
```
|
|
|
|
#### D. Publishing Events
|
|
```javascript
|
|
// Publish event via worker
|
|
async function publishEvent(unsignedEvent) {
|
|
return new Promise((resolve, reject) => {
|
|
const requestId = generateRequestId();
|
|
|
|
// Set up response handler
|
|
const handler = (event) => {
|
|
if (event.detail.requestId === requestId) {
|
|
window.removeEventListener('ndkPublishResponse', handler);
|
|
if (event.detail.error) {
|
|
reject(new Error(event.detail.error));
|
|
} else {
|
|
resolve(event.detail.result);
|
|
}
|
|
}
|
|
};
|
|
|
|
window.addEventListener('ndkPublishResponse', handler);
|
|
|
|
// Send publish request to worker
|
|
ndkWorker.port.postMessage({
|
|
type: 'publish',
|
|
requestId: requestId,
|
|
event: unsignedEvent
|
|
});
|
|
});
|
|
}
|
|
```
|
|
|
|
### Step 6: Distribution Strategy
|
|
|
|
**How Pages Can Be Distributed:**
|
|
|
|
#### Option 1: Complete Suite
|
|
Distribute entire `client/` directory:
|
|
```
|
|
client/
|
|
├── index.html (launcher)
|
|
├── ndk_worker.mjs
|
|
├── js/ (all shared code)
|
|
├── html/ (all pages)
|
|
├── css/ (all styles)
|
|
└── nostr_login_lite/
|
|
```
|
|
User gets full suite with launcher.
|
|
|
|
#### Option 2: Individual Pages
|
|
Distribute single page with dependencies:
|
|
```
|
|
profile-tool/
|
|
├── profile.html (renamed to index.html)
|
|
├── ndk_worker.mjs
|
|
├── js/
|
|
│ ├── init-ndk.mjs
|
|
│ └── ndk-core.bundle.js
|
|
├── css/
|
|
│ └── client.css
|
|
└── nostr_login_lite/
|
|
└── build/
|
|
└── nostr-lite.js
|
|
```
|
|
User gets just the profile editor as standalone tool.
|
|
|
|
#### Option 3: Hybrid Distribution
|
|
Distribute subset of pages:
|
|
```
|
|
messaging-suite/
|
|
├── index.html (custom launcher for just messaging tools)
|
|
├── ndk_worker.mjs
|
|
├── js/ (shared code)
|
|
├── html/
|
|
│ ├── msg.html
|
|
│ ├── post.html
|
|
│ └── feed.html
|
|
├── css/
|
|
└── nostr_login_lite/
|
|
```
|
|
User gets curated set of related tools.
|
|
|
|
**Key Benefits:**
|
|
- Each page is independently functional
|
|
- Shared worker prevents resource duplication when multiple pages open
|
|
- Shared cache means data persists across pages
|
|
- Users can bookmark specific tools
|
|
- Developers can distribute just what users need
|
|
|
|
---
|
|
|
|
## NDK Cache Schema vs Custom Schema
|
|
|
|
### Custom DB_NOSTR Schema
|
|
```javascript
|
|
Event_0: "&pubkey" // Profile metadata
|
|
Event_1: "&id, pubkey, created_at" // Notes
|
|
Event_3: "&pubkey" // Contacts
|
|
Event_10002: "&pubkey" // Relay list
|
|
Event_10063: "&pubkey" // User server list
|
|
Event_4: "&id, pubkey, created_at, p" // DMs
|
|
Event_30023: "[pubkey+d], pubkey, id" // Long-form
|
|
Event_30024: "[pubkey+d], pubkey, id" // Drafts
|
|
Event_30078: "&d, pubkey, id" // App-specific
|
|
Relays: "&relay, read, write, order, visible, connected"
|
|
```
|
|
|
|
### NDK Dexie Cache Schema
|
|
```javascript
|
|
events: "id, kind, pubkey, created_at, [kind+pubkey], [kind+created_at]"
|
|
eventTags: "[eventId+tag+value], eventId, tag, value"
|
|
profiles: "pubkey"
|
|
nip05: "nip05, fetchedAt"
|
|
relayStatus: "url"
|
|
unpublishedEvents: "++id, eventId"
|
|
eventRelays: "[eventId+relayUrl], eventId, relayUrl"
|
|
```
|
|
|
|
**Key Differences:**
|
|
- NDK uses unified `events` table instead of per-kind tables
|
|
- NDK has `eventTags` for efficient tag queries
|
|
- NDK tracks relay status and event relay associations
|
|
- NDK has unpublished events queue
|
|
|
|
---
|
|
|
|
## Testing Strategy
|
|
|
|
### Phase 1: Basic Functionality
|
|
1. Authentication works with nostr-login-lite
|
|
2. NDK worker initializes successfully
|
|
3. Profile loads from cache and relays
|
|
4. Event 10002 loads and updates relay pool
|
|
5. Event 3 (contacts) loads successfully
|
|
|
|
### Phase 2: Subscriptions
|
|
1. User metadata subscription works
|
|
2. Follows metadata subscription works
|
|
3. Feed subscription works
|
|
4. DM subscription works
|
|
|
|
### Phase 3: Publishing
|
|
1. Can publish notes (kind 1)
|
|
2. Can publish profile updates (kind 0)
|
|
3. Can publish relay list (kind 10002)
|
|
4. Can publish DMs (kind 4)
|
|
|
|
### Phase 4: Multi-tab & Independence
|
|
1. Multiple tabs share same NDK worker
|
|
2. Events appear in all tabs
|
|
3. Logout works across all tabs
|
|
4. **Can access any page directly without launcher**
|
|
5. **Authentication works from any entry point**
|
|
6. **Shared cache works across different pages**
|
|
|
|
### Phase 5: Performance
|
|
1. Cache-first loading is fast
|
|
2. Relay connections are stable
|
|
3. No memory leaks
|
|
4. Subscription deduplication works
|
|
|
|
### Phase 6: Distribution
|
|
1. Individual page distribution works
|
|
2. All dependencies included
|
|
3. Pages work standalone
|
|
4. Pages work together when multiple open
|
|
|
|
---
|
|
|
|
## Implementation Checklist
|
|
|
|
### Preparation
|
|
- [ ] Copy `www/ndk-worker.js` to `client/ndk_worker.mjs` as template
|
|
- [ ] Copy `www/ndk-core.bundle.js` to `client/js/`
|
|
- [ ] Create `client/js/init-ndk.mjs` with path-aware worker loading
|
|
|
|
### Core Implementation
|
|
- [ ] Implement NDK worker with Dexie cache
|
|
- [ ] Implement authentication flow in init-ndk.mjs
|
|
- [ ] Add automatic nostr-login-lite launch if not authenticated
|
|
- [ ] Add path-aware worker loading (works from /client/ and /client/html/)
|
|
- [ ] Implement profile subscription (kind 0)
|
|
- [ ] Implement relay list subscription (kind 10002)
|
|
- [ ] Implement contacts subscription (kind 3)
|
|
- [ ] Implement event publishing
|
|
|
|
### Page Templates
|
|
- [ ] Create standalone page template
|
|
- [ ] Create launcher page (index.html)
|
|
- [ ] Test standalone page can be accessed directly
|
|
- [ ] Test authentication works from any page
|
|
- [ ] Test shared worker connection from different pages
|
|
|
|
### Individual Pages
|
|
- [ ] Migrate profile.html as standalone page
|
|
- [ ] Migrate feed.html as standalone page
|
|
- [ ] Migrate msg.html as standalone page
|
|
- [ ] Migrate post.html as standalone page
|
|
- [ ] Migrate relays.html as standalone page
|
|
- [ ] Migrate db.html as standalone page
|
|
- [ ] Migrate remaining pages in `client/html/`
|
|
|
|
### Testing
|
|
- [ ] Test accessing each page directly (not through launcher)
|
|
- [ ] Test authentication from different entry points
|
|
- [ ] Test shared worker across multiple pages
|
|
- [ ] Test shared cache across multiple pages
|
|
- [ ] Test multi-tab functionality
|
|
- [ ] Test logout across tabs
|
|
- [ ] Test distributing individual pages
|
|
|
|
### Distribution Testing
|
|
- [ ] Test complete suite distribution
|
|
- [ ] Test individual page distribution
|
|
- [ ] Test subset distribution
|
|
- [ ] Verify all dependencies included in distributions
|
|
- [ ] Test pages work standalone
|
|
- [ ] Test pages work together when multiple open
|
|
|
|
---
|
|
|
|
## Risk Assessment
|
|
|
|
### High Risk
|
|
- **Breaking existing functionality**: Replacing entire implementation
|
|
- *Mitigation*: Thorough testing before deployment
|
|
|
|
### Medium Risk
|
|
- **Performance differences**: NDK might perform differently than custom code
|
|
- *Mitigation*: Performance testing, can optimize NDK usage
|
|
|
|
- **Learning curve**: Team needs to learn NDK API
|
|
- *Mitigation*: Good documentation, incremental migration
|
|
|
|
- **Path resolution issues**: Worker path must work from different page locations
|
|
- *Mitigation*: Test from all page locations, use path-aware loading
|
|
|
|
### Low Risk
|
|
- **Authentication compatibility**: nostr-login-lite should work the same
|
|
- *Mitigation*: Keep existing auth pattern
|
|
|
|
- **Multi-tab issues**: NDK SharedWorker pattern already proven in www/
|
|
- *Mitigation*: Use proven pattern from www/ implementation
|
|
|
|
---
|
|
|
|
## Success Criteria
|
|
|
|
### Must Have
|
|
✅ Authentication works with nostr-login-lite
|
|
✅ Profile loads from cache and relays
|
|
✅ Relay list (Event 10002) loads and updates NDK pool
|
|
✅ Can publish events
|
|
✅ Multi-tab support works
|
|
✅ Logout works across all tabs
|
|
✅ **Each page can be accessed directly**
|
|
✅ **Authentication works from any entry point**
|
|
✅ **Shared worker works across all pages**
|
|
|
|
### Should Have
|
|
✅ Performance equal to or better than custom implementation
|
|
✅ All 15+ tool pages migrated
|
|
✅ **Individual page distribution works**
|
|
✅ **Pages work standalone and together**
|
|
|
|
### Nice to Have
|
|
✅ Improved relay selection using NDK outbox model
|
|
✅ Better error handling and retry logic
|
|
✅ Reduced codebase size
|
|
|
|
---
|
|
|
|
## Next Steps
|
|
|
|
1. **Set up development environment** with NDK bundle
|
|
2. **Start with NDK worker implementation** (`client/ndk_worker.mjs`)
|
|
3. **Create init-ndk.mjs with path-aware worker loading**
|
|
4. **Create standalone page template**
|
|
5. **Test standalone page can be accessed directly**
|
|
6. **Create launcher page** (`client/index.html`)
|
|
7. **Migrate individual pages** one by one
|
|
8. **Test distribution scenarios**
|
|
|
|
---
|
|
|
|
## Questions to Resolve
|
|
|
|
1. **Database Name**: Use `ndk-client` for shared database
|
|
2. **Worker Path Resolution**: Ensure worker path works from all page locations
|
|
3. **Dependency Bundling**: How to package dependencies for individual page distribution?
|
|
4. **Feature Parity**: Are there custom features that NDK doesn't support?
|
|
5. **Distribution Format**: Zip files? Git repos? NPM packages?
|
|
|
|
---
|
|
|
|
## Appendix: Code Comparison Examples
|
|
|
|
### Example 1: Connecting to Relays
|
|
|
|
**Custom Implementation:**
|
|
```javascript
|
|
// Manual WebSocket management
|
|
const OpenRelayConnection = async (relay) => {
|
|
OBJ_SOCKETS[relay] = new WebSocket(relay);
|
|
|
|
OBJ_SOCKETS[relay].onopen = function (event) {
|
|
wsOnOpen(event, relay);
|
|
};
|
|
|
|
OBJ_SOCKETS[relay].onmessage = function (event) {
|
|
wsOnMessage(event, relay);
|
|
};
|
|
|
|
OBJ_SOCKETS[relay].onclose = function (event) {
|
|
wsOnClose(event, relay);
|
|
};
|
|
|
|
OBJ_SOCKETS[relay].onerror = function (error) {
|
|
console.log(error, relay);
|
|
};
|
|
};
|
|
```
|
|
|
|
**NDK Implementation:**
|
|
```javascript
|
|
// NDK handles everything
|
|
const ndk = new NDK({
|
|
explicitRelayUrls: relayUrls,
|
|
cacheAdapter: cacheAdapter
|
|
});
|
|
|
|
await ndk.connect();
|
|
// That's it! NDK manages connections, reconnections, health checks
|
|
```
|
|
|
|
### Example 2: Subscribing to Events
|
|
|
|
**Custom Implementation:**
|
|
```javascript
|
|
// Build subscription manually
|
|
const subscription = await arrGenerateSubscription(subId);
|
|
const socket = OBJ_SOCKETS[relay];
|
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
socket.send(JSON.stringify(subscription));
|
|
}
|
|
|
|
// Handle responses in onmessage
|
|
socket.onmessage = async (event) => {
|
|
const msg = JSON.parse(event.data);
|
|
if (msg[0] === "EVENT") {
|
|
await DB_NOSTR.Event_0.put(msg[2]);
|
|
}
|
|
};
|
|
```
|
|
|
|
**NDK Implementation:**
|
|
```javascript
|
|
// NDK handles subscription, deduplication, caching
|
|
const sub = ndk.subscribe(
|
|
{ kinds: [0], authors: [pubkey] },
|
|
{ closeOnEose: false, cacheUsage: 'CACHE_FIRST' }
|
|
);
|
|
|
|
sub.on('event', (event) => {
|
|
// Event is already cached by NDK
|
|
updateUI(event);
|
|
});
|
|
```
|
|
|
|
### Example 3: Publishing Events
|
|
|
|
**Custom Implementation:**
|
|
```javascript
|
|
// Manual signing and publishing
|
|
const signedEvent = await requestEventSigning(event);
|
|
event.id = signedEvent.id;
|
|
event.sig = signedEvent.sig;
|
|
event.pubkey = signedEvent.pubkey;
|
|
|
|
const arrReq = ["EVENT", event];
|
|
|
|
for (let relay of await DB_NOSTR.Relays.where({ write: 1, connected: 1 }).toArray()) {
|
|
await OBJ_SOCKETS[relay.relay].send(JSON.stringify(arrReq));
|
|
}
|
|
```
|
|
|
|
**NDK Implementation:**
|
|
```javascript
|
|
// NDK handles signing, relay selection, publishing
|
|
const ndkEvent = new NDKEvent(ndk, event);
|
|
await ndkEvent.sign(signer);
|
|
await ndkEvent.publish();
|
|
// NDK automatically selects best relays and handles retries
|
|
```
|
|
|
|
### Example 4: Accessing Pages Independently
|
|
|
|
**Custom Implementation:**
|
|
```javascript
|
|
// Must go through launcher
|
|
// Direct access to /client/html/profile.html doesn't work
|
|
// Requires index.html to initialize worker first
|
|
```
|
|
|
|
**NDK Implementation:**
|
|
```javascript
|
|
// Can access any page directly
|
|
// User visits /client/html/profile.html
|
|
// Page checks authentication
|
|
// If not authenticated, shows nostr-login-lite
|
|
// After auth, connects to shared worker
|
|
// Works immediately, no launcher needed
|
|
|
|
// But launcher still available for convenience
|
|
// User can also visit /client/index.html for app grid
|
|
```
|
|
|
|
---
|
|
|
|
## Conclusion
|
|
|
|
This migration will significantly simplify the codebase while improving reliability and maintainability. The distributed architecture allows each page to be independently accessible while sharing resources through the NDK SharedWorker.
|
|
|
|
**Key Architectural Benefits:**
|
|
- **Independent Access**: Users can go directly to any tool
|
|
- **Shared Resources**: All pages share same worker and cache
|
|
- **Flexible Distribution**: Distribute complete suite, individual pages, or subsets
|
|
- **No Duplication**: Shared worker prevents duplicate relay connections
|
|
- **Instant Data**: Shared cache means data available across all pages
|
|
- **Modular Design**: Easy to add, remove, or distribute individual tools
|
|
|
|
The key to success is:
|
|
1. Get the NDK worker working first
|
|
2. Create a robust init-ndk.mjs that handles authentication from any page
|
|
3. Implement path-aware worker loading
|
|
4. Create one standalone page template
|
|
5. Replicate template for all other pages
|
|
6. Test each page can be accessed independently
|
|
7. Test pages work together when multiple are open
|
|
8. Test distribution scenarios
|