nrc-session.ts raw

   1  import { Filter } from 'nostr-tools'
   2  import { NRCSession, NRCSubscription } from './nrc-types'
   3  
   4  // Default session timeout: 30 minutes
   5  const DEFAULT_SESSION_TIMEOUT = 30 * 60 * 1000
   6  
   7  // Default max subscriptions per session
   8  const DEFAULT_MAX_SUBSCRIPTIONS = 100
   9  
  10  /**
  11   * Generate a unique session ID
  12   */
  13  function generateSessionId(): string {
  14    return crypto.randomUUID()
  15  }
  16  
  17  /**
  18   * Session manager for tracking NRC client sessions
  19   */
  20  export class NRCSessionManager {
  21    private sessions: Map<string, NRCSession> = new Map()
  22    private sessionTimeout: number
  23    private maxSubscriptions: number
  24    private cleanupInterval: ReturnType<typeof setInterval> | null = null
  25  
  26    constructor(
  27      sessionTimeout: number = DEFAULT_SESSION_TIMEOUT,
  28      maxSubscriptions: number = DEFAULT_MAX_SUBSCRIPTIONS
  29    ) {
  30      this.sessionTimeout = sessionTimeout
  31      this.maxSubscriptions = maxSubscriptions
  32    }
  33  
  34    /**
  35     * Start the cleanup interval for expired sessions
  36     */
  37    start(): void {
  38      if (this.cleanupInterval) return
  39  
  40      // Run cleanup every 5 minutes
  41      this.cleanupInterval = setInterval(() => {
  42        this.cleanupExpiredSessions()
  43      }, 5 * 60 * 1000)
  44    }
  45  
  46    /**
  47     * Stop the cleanup interval
  48     */
  49    stop(): void {
  50      if (this.cleanupInterval) {
  51        clearInterval(this.cleanupInterval)
  52        this.cleanupInterval = null
  53      }
  54      this.sessions.clear()
  55    }
  56  
  57    /**
  58     * Get or create a session for a client
  59     */
  60    getOrCreateSession(
  61      clientPubkey: string,
  62      conversationKey: Uint8Array | undefined,
  63      deviceName?: string
  64    ): NRCSession {
  65      // Check if session exists for this client
  66      for (const session of this.sessions.values()) {
  67        if (session.clientPubkey === clientPubkey) {
  68          // Update last activity and return existing session
  69          session.lastActivity = Date.now()
  70          return session
  71        }
  72      }
  73  
  74      // Create new session
  75      const session: NRCSession = {
  76        id: generateSessionId(),
  77        clientPubkey,
  78        conversationKey,
  79        deviceName,
  80        createdAt: Date.now(),
  81        lastActivity: Date.now(),
  82        subscriptions: new Map()
  83      }
  84  
  85      this.sessions.set(session.id, session)
  86      return session
  87    }
  88  
  89    /**
  90     * Get a session by ID
  91     */
  92    getSession(sessionId: string): NRCSession | undefined {
  93      return this.sessions.get(sessionId)
  94    }
  95  
  96    /**
  97     * Get a session by client pubkey
  98     */
  99    getSessionByClientPubkey(clientPubkey: string): NRCSession | undefined {
 100      for (const session of this.sessions.values()) {
 101        if (session.clientPubkey === clientPubkey) {
 102          return session
 103        }
 104      }
 105      return undefined
 106    }
 107  
 108    /**
 109     * Touch a session to update last activity
 110     */
 111    touchSession(sessionId: string): void {
 112      const session = this.sessions.get(sessionId)
 113      if (session) {
 114        session.lastActivity = Date.now()
 115      }
 116    }
 117  
 118    /**
 119     * Add a subscription to a session
 120     */
 121    addSubscription(
 122      sessionId: string,
 123      subId: string,
 124      filters: Filter[]
 125    ): NRCSubscription | null {
 126      const session = this.sessions.get(sessionId)
 127      if (!session) return null
 128  
 129      // Check subscription limit
 130      if (session.subscriptions.size >= this.maxSubscriptions) {
 131        return null
 132      }
 133  
 134      const subscription: NRCSubscription = {
 135        id: subId,
 136        filters,
 137        createdAt: Date.now(),
 138        eventCount: 0,
 139        eoseSent: false
 140      }
 141  
 142      session.subscriptions.set(subId, subscription)
 143      session.lastActivity = Date.now()
 144  
 145      return subscription
 146    }
 147  
 148    /**
 149     * Get a subscription from a session
 150     */
 151    getSubscription(sessionId: string, subId: string): NRCSubscription | undefined {
 152      const session = this.sessions.get(sessionId)
 153      return session?.subscriptions.get(subId)
 154    }
 155  
 156    /**
 157     * Remove a subscription from a session
 158     */
 159    removeSubscription(sessionId: string, subId: string): boolean {
 160      const session = this.sessions.get(sessionId)
 161      if (!session) return false
 162  
 163      const deleted = session.subscriptions.delete(subId)
 164      if (deleted) {
 165        session.lastActivity = Date.now()
 166      }
 167      return deleted
 168    }
 169  
 170    /**
 171     * Mark EOSE sent for a subscription
 172     */
 173    markEOSE(sessionId: string, subId: string): void {
 174      const subscription = this.getSubscription(sessionId, subId)
 175      if (subscription) {
 176        subscription.eoseSent = true
 177      }
 178    }
 179  
 180    /**
 181     * Increment event count for a subscription
 182     */
 183    incrementEventCount(sessionId: string, subId: string): void {
 184      const subscription = this.getSubscription(sessionId, subId)
 185      if (subscription) {
 186        subscription.eventCount++
 187      }
 188    }
 189  
 190    /**
 191     * Remove a session
 192     */
 193    removeSession(sessionId: string): boolean {
 194      return this.sessions.delete(sessionId)
 195    }
 196  
 197    /**
 198     * Get the count of active sessions
 199     */
 200    getActiveSessionCount(): number {
 201      return this.sessions.size
 202    }
 203  
 204    /**
 205     * Get all active sessions
 206     */
 207    getAllSessions(): NRCSession[] {
 208      return Array.from(this.sessions.values())
 209    }
 210  
 211    /**
 212     * Clean up expired sessions
 213     */
 214    private cleanupExpiredSessions(): void {
 215      const now = Date.now()
 216      const expiredSessionIds: string[] = []
 217  
 218      for (const [sessionId, session] of this.sessions) {
 219        if (now - session.lastActivity > this.sessionTimeout) {
 220          expiredSessionIds.push(sessionId)
 221        }
 222      }
 223  
 224      for (const sessionId of expiredSessionIds) {
 225        this.sessions.delete(sessionId)
 226        console.log(`[NRC] Cleaned up expired session: ${sessionId}`)
 227      }
 228    }
 229  
 230    /**
 231     * Check if a session is expired
 232     */
 233    isSessionExpired(sessionId: string): boolean {
 234      const session = this.sessions.get(sessionId)
 235      if (!session) return true
 236      return Date.now() - session.lastActivity > this.sessionTimeout
 237    }
 238  }
 239