1
0
mirror of https://github.com/DocNR/POWR.git synced 2025-05-13 17:55:53 +00:00
POWR/docs/technical/ndk/subscription_analysis.md

10 KiB

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:

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

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

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

// Start the subscription
subscription.start();

// When no longer needed
subscription.stop();

4. Error Handling

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

// 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

// 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

// 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

// 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:

// 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

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

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:

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

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

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