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