mirror of
https://github.com/DocNR/POWR.git
synced 2025-05-14 02:05:53 +00:00
371 lines
10 KiB
Markdown
371 lines
10 KiB
Markdown
# NDK Subscription Analysis
|
|
|
|
**Last Updated:** 2025-03-26
|
|
**Status:** Active
|
|
**Related To:** Nostr Integration, Social Feed, Contact Lists
|
|
|
|
## Purpose
|
|
|
|
This document analyzes the NDK subscription system, focusing on best practices for managing subscriptions, contact lists, and related event handling. It provides guidelines for reliable implementation in the POWR app, particularly for social features that depend on these subscription mechanisms.
|
|
|
|
## NDK Subscription Overview
|
|
|
|
Subscriptions in NDK provide a mechanism to retrieve and monitor events from Nostr relays. The subscription system is built around the `NDKSubscription` class, which manages connections to relays, event caching, and event emission.
|
|
|
|
### Key Subscription Concepts
|
|
|
|
1. **Filters**: Define what events to retrieve (kind, authors, tags, etc.)
|
|
2. **Options**: Control subscription behavior (caching, verification, grouping)
|
|
3. **Event Handlers**: Process events as they arrive
|
|
4. **Cache Integration**: Improve performance and offline capabilities
|
|
5. **Relay Management**: Control which relays receive subscription requests
|
|
|
|
## Subscription Options
|
|
|
|
NDK offers several configuration options to customize subscription behavior:
|
|
|
|
```typescript
|
|
interface NDKSubscriptionOptions {
|
|
// Whether to close subscription when all relays have sent EOSE
|
|
closeOnEose?: boolean;
|
|
|
|
// How to use the cache
|
|
cacheUsage?: NDKSubscriptionCacheUsage;
|
|
|
|
// Whether to skip saving events to cache
|
|
dontSaveToCache?: boolean;
|
|
|
|
// Group similar subscriptions to reduce relay connections
|
|
groupable?: boolean;
|
|
|
|
// How long to wait before sending grouped subscriptions
|
|
groupableDelay?: number;
|
|
|
|
// Verification and validation controls
|
|
skipVerification?: boolean;
|
|
skipValidation?: boolean;
|
|
|
|
// Cache-specific filtering options
|
|
cacheUnconstrainFilter?: (keyof NDKFilter)[];
|
|
}
|
|
```
|
|
|
|
### Cache Usage Modes
|
|
|
|
- `CACHE_FIRST`: Try cache before hitting relays (default)
|
|
- `PARALLEL`: Query both cache and relays simultaneously
|
|
- `ONLY_CACHE`: Only use cached data, don't hit relays
|
|
- `ONLY_RELAY`: Skip cache entirely, always query relays
|
|
|
|
## Best Practices for Subscriptions
|
|
|
|
### 1. Effective Subscription Configuration
|
|
|
|
```typescript
|
|
const subscriptionOptions = {
|
|
closeOnEose: true, // Close when all relays send EOSE
|
|
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST, // Try cache before hitting relays
|
|
groupable: true, // Allow grouping similar subscriptions
|
|
groupableDelay: 100, // Wait 100ms to group subscriptions
|
|
skipVerification: false, // Verify signatures
|
|
skipValidation: false // Validate event structure
|
|
};
|
|
|
|
const subscription = ndk.subscribe(
|
|
{ kinds: [1, 3], // Post and contact list events
|
|
authors: [userPubkey] },
|
|
subscriptionOptions
|
|
);
|
|
```
|
|
|
|
### 2. Event Handling
|
|
|
|
```typescript
|
|
subscription.on('event', (event, relay, sub, fromCache, optimisticPublish) => {
|
|
// Process event
|
|
|
|
// Check if this is from cache
|
|
if (fromCache) {
|
|
// Handle cached event (e.g., for initial UI render)
|
|
} else {
|
|
// Handle live event from relay (e.g., for updates)
|
|
}
|
|
});
|
|
|
|
subscription.on('eose', (sub) => {
|
|
// End of stored events - all historical data has been received
|
|
// Good time to hide loading indicators
|
|
});
|
|
|
|
subscription.on('close', (sub) => {
|
|
// Subscription closed - clean up any resources
|
|
});
|
|
```
|
|
|
|
### 3. Subscription Lifecycle Management
|
|
|
|
```typescript
|
|
// Start the subscription
|
|
subscription.start();
|
|
|
|
// When no longer needed
|
|
subscription.stop();
|
|
```
|
|
|
|
### 4. Error Handling
|
|
|
|
```typescript
|
|
subscription.on('error', (error, relay) => {
|
|
console.error(`Subscription error from ${relay.url}:`, error);
|
|
// Implement fallback strategy
|
|
});
|
|
|
|
subscription.on('closed', (relay, reason) => {
|
|
console.log(`Relay ${relay.url} closed connection: ${reason}`);
|
|
// Potentially reconnect or use alternative relays
|
|
});
|
|
```
|
|
|
|
## Contact List Handling
|
|
|
|
The `NDKUser` class provides utilities for working with contact lists (kind:3 events).
|
|
|
|
### Retrieving Contacts
|
|
|
|
```typescript
|
|
// Initialize user
|
|
const user = new NDKUser({ pubkey: userPubkey });
|
|
user.ndk = ndk;
|
|
|
|
// Get contacts as NDKUser objects
|
|
const follows = await user.follows();
|
|
|
|
// Get just the pubkeys
|
|
const followSet = await user.followSet();
|
|
```
|
|
|
|
### Managing Contacts
|
|
|
|
```typescript
|
|
// Add a new follow
|
|
const newFollow = new NDKUser({ pubkey: followPubkey });
|
|
await user.follow(newFollow);
|
|
|
|
// Remove a follow
|
|
await user.unfollow(userToUnfollow);
|
|
|
|
// Batch multiple operations
|
|
const currentFollows = await user.follows();
|
|
await user.follow(newFollow, currentFollows);
|
|
await user.follow(anotherFollow, currentFollows);
|
|
```
|
|
|
|
## Performance Optimization Patterns
|
|
|
|
### 1. Subscription Grouping
|
|
|
|
```typescript
|
|
// These will be grouped into a single subscription to the relay
|
|
// if created within 100ms of each other
|
|
const sub1 = ndk.subscribe(
|
|
{ kinds: [1], authors: [pubkey1] },
|
|
{ groupable: true, groupableDelay: 100 }
|
|
);
|
|
|
|
const sub2 = ndk.subscribe(
|
|
{ kinds: [1], authors: [pubkey2] },
|
|
{ groupable: true, groupableDelay: 100 }
|
|
);
|
|
```
|
|
|
|
### 2. Progressive Loading
|
|
|
|
```typescript
|
|
// Initial UI state from cache
|
|
const sub = ndk.subscribe(
|
|
{ kinds: [3], authors: [userPubkey] },
|
|
{ cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST }
|
|
);
|
|
|
|
// Update as data comes in
|
|
sub.on('event', (event, relay, subscription, fromCache) => {
|
|
if (fromCache) {
|
|
// Update UI immediately with cached data
|
|
} else {
|
|
// Refresh UI with the latest data from relays
|
|
}
|
|
});
|
|
```
|
|
|
|
### 3. Targeted Subscriptions
|
|
|
|
Limit the scope of subscriptions to reduce data load:
|
|
|
|
```typescript
|
|
// Use since/until filters to limit time range
|
|
const recentSub = ndk.subscribe({
|
|
kinds: [1],
|
|
authors: [userPubkey],
|
|
since: Math.floor(Date.now() / 1000) - 86400 // Last 24 hours
|
|
});
|
|
|
|
// Use specific tag filters
|
|
const hashtagSub = ndk.subscribe({
|
|
kinds: [1],
|
|
"#t": ["workout", "fitness"]
|
|
});
|
|
```
|
|
|
|
## Reliability Implementation
|
|
|
|
### 1. Contact List Manager Class
|
|
|
|
```typescript
|
|
class ContactListManager {
|
|
private contactList = new Set<string>();
|
|
private subscription;
|
|
private lastUpdated = 0;
|
|
|
|
constructor(ndk, userPubkey) {
|
|
this.subscription = ndk.subscribe({
|
|
kinds: [3],
|
|
authors: [userPubkey]
|
|
});
|
|
|
|
this.subscription.on('event', this.handleContactListUpdate.bind(this));
|
|
}
|
|
|
|
private handleContactListUpdate(event) {
|
|
// Only update if this is a newer event
|
|
if (event.created_at > this.lastUpdated) {
|
|
this.contactList = new Set(
|
|
event.tags
|
|
.filter(tag => tag[0] === 'p')
|
|
.map(tag => tag[1])
|
|
);
|
|
this.lastUpdated = event.created_at;
|
|
}
|
|
}
|
|
|
|
getContacts() {
|
|
return this.contactList;
|
|
}
|
|
|
|
cleanup() {
|
|
this.subscription.stop();
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. Implementing Retry Logic
|
|
|
|
```typescript
|
|
async function fetchContactListWithRetry(attempts = 3) {
|
|
for (let i = 0; i < attempts; i++) {
|
|
try {
|
|
const user = new NDKUser({ pubkey: userPubkey });
|
|
user.ndk = ndk;
|
|
return await user.followSet();
|
|
} catch (e) {
|
|
if (i === attempts - 1) throw e;
|
|
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Custom Cache Implementation
|
|
|
|
For optimal performance, implement a custom cache adapter:
|
|
|
|
```typescript
|
|
const cacheAdapter = {
|
|
async query(subscription) {
|
|
// Return cached events matching subscription filters
|
|
const results = await db.query(
|
|
'SELECT * FROM events WHERE...',
|
|
// Convert subscription filters to query
|
|
);
|
|
return results.map(row => new NDKEvent(ndk, JSON.parse(row.event)));
|
|
},
|
|
|
|
async setEvent(event, filters, relay) {
|
|
// Store event in cache
|
|
await db.run(
|
|
'INSERT OR REPLACE INTO events VALUES...',
|
|
[event.id, JSON.stringify(event), /* other fields */]
|
|
);
|
|
}
|
|
};
|
|
|
|
ndk.cacheAdapter = cacheAdapter;
|
|
```
|
|
|
|
## Application to POWR Social Features
|
|
|
|
### Social Feed Implementation
|
|
|
|
```typescript
|
|
function initializeSocialFeed() {
|
|
// Following feed
|
|
const followingFeed = ndk.subscribe({
|
|
kinds: [1, 6], // Posts and reposts
|
|
"#p": Array.from(contactList) // People the user follows
|
|
}, {
|
|
closeOnEose: false, // Keep subscription open for updates
|
|
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST
|
|
});
|
|
|
|
followingFeed.on('event', (event) => {
|
|
// Process feed events
|
|
updateFeedUI(event);
|
|
});
|
|
|
|
// Global feed (limited to recent events)
|
|
const globalFeed = ndk.subscribe({
|
|
kinds: [1, 6],
|
|
since: Math.floor(Date.now() / 1000) - 86400 // Last 24 hours
|
|
});
|
|
|
|
globalFeed.on('event', (event) => {
|
|
// Process global feed events
|
|
updateGlobalFeedUI(event);
|
|
});
|
|
}
|
|
```
|
|
|
|
### Workout Sharing & Discovery
|
|
|
|
```typescript
|
|
function initializeWorkoutFeed() {
|
|
// Workout-specific feed
|
|
const workoutFeed = ndk.subscribe({
|
|
kinds: [1301], // Workout records (from NIP-4e)
|
|
"#t": ["workout", "fitness"] // Relevant hashtags
|
|
});
|
|
|
|
workoutFeed.on('event', (event) => {
|
|
// Process workout events
|
|
updateWorkoutFeedUI(event);
|
|
});
|
|
}
|
|
```
|
|
|
|
## Conclusion
|
|
|
|
The NDK subscription system provides a powerful foundation for Nostr data management. By following these best practices, the POWR app can implement reliable, efficient data flows for social features, contact lists, and other Nostr-based functionality.
|
|
|
|
Key recommendations:
|
|
1. Use proper subscription options for each use case
|
|
2. Implement effective caching strategies
|
|
3. Handle subscription lifecycle properly
|
|
4. Use the `NDKUser` class for contact list operations
|
|
5. Group similar subscriptions where possible
|
|
6. Implement robust error handling and retry logic
|
|
|
|
## Related Documentation
|
|
|
|
- [NDK Comprehensive Guide](./comprehensive_guide.md) - Complete guide to NDK functionality
|
|
- [Nostr Exercise NIP](../nostr/exercise_nip.md) - Specification for workout events
|
|
- [Social Architecture](../../features/social/architecture.md) - Overall social feature architecture
|