@@ -42,14 +42,25 @@ interface PrimalFeedResponse {
4242 profiles : PrimalProfile [ ] ;
4343}
4444
45+ export interface PrimalUserStats {
46+ followers_count : number ;
47+ follows_count : number ;
48+ note_count : number ;
49+ reply_count : number ;
50+ total_zap_count : number ;
51+ total_satszapped : number ;
52+ time_joined : number ;
53+ }
54+
4555interface PendingRequest {
46- resolve : ( value : PrimalSearchResult | PrimalFeedResponse | string [ ] ) => void ;
56+ resolve : ( value : PrimalSearchResult | PrimalFeedResponse | string [ ] | PrimalUserStats | null ) => void ;
4757 reject : ( error : Error ) => void ;
4858 timeout : ReturnType < typeof setTimeout > ;
4959 profiles : PrimalProfile [ ] ;
5060 events : PrimalEvent [ ] ;
5161 follows : string [ ] ;
52- type : 'search' | 'feed' | 'contacts' | 'global' | 'articles' ;
62+ userStats : PrimalUserStats | null ;
63+ type : 'search' | 'feed' | 'contacts' | 'global' | 'articles' | 'user_stats' ;
5364}
5465
5566export interface PrimalArticleOptions {
@@ -186,6 +197,22 @@ export class PrimalCacheService {
186197 . filter ( ( tag : string [ ] ) => tag [ 0 ] === 'p' && tag [ 1 ] )
187198 . map ( ( tag : string [ ] ) => tag [ 1 ] ) ;
188199 pending . follows . push ( ...follows ) ;
200+ } else if ( event . kind === 10000105 ) {
201+ // User stats (Primal-specific kind)
202+ try {
203+ const stats = JSON . parse ( event . content ) ;
204+ pending . userStats = {
205+ followers_count : stats . followers_count ?? 0 ,
206+ follows_count : stats . follows_count ?? 0 ,
207+ note_count : stats . note_count ?? 0 ,
208+ reply_count : stats . reply_count ?? 0 ,
209+ total_zap_count : stats . total_zap_count ?? 0 ,
210+ total_satszapped : stats . total_satszapped ?? 0 ,
211+ time_joined : stats . time_joined ?? 0
212+ } ;
213+ } catch ( error ) {
214+ console . error ( '[PrimalCache] Error parsing user stats:' , error ) ;
215+ }
189216 }
190217 } else if ( messageType === 'EOSE' && requestId ) {
191218 const pending = this . pendingRequests . get ( requestId as string ) ;
@@ -201,6 +228,8 @@ export class PrimalCacheService {
201228 } ) ;
202229 } else if ( pending . type === 'contacts' ) {
203230 ( pending . resolve as ( value : string [ ] ) => void ) ( pending . follows ) ;
231+ } else if ( pending . type === 'user_stats' ) {
232+ ( pending . resolve as ( value : PrimalUserStats | null ) => void ) ( pending . userStats ) ;
204233 }
205234
206235 this . pendingRequests . delete ( requestId as string ) ;
@@ -249,6 +278,7 @@ export class PrimalCacheService {
249278 profiles : [ ] ,
250279 events : [ ] ,
251280 follows : [ ] ,
281+ userStats : null ,
252282 type : 'search'
253283 } ) ;
254284
@@ -294,12 +324,13 @@ export class PrimalCacheService {
294324 } , timeoutMs ) ;
295325
296326 this . pendingRequests . set ( requestId , {
297- resolve : resolve as ( value : PrimalSearchResult | PrimalFeedResponse | string [ ] ) => void ,
327+ resolve : ( value ) => resolve ( value as string [ ] ) ,
298328 reject,
299329 timeout,
300330 profiles : [ ] ,
301331 events : [ ] ,
302332 follows : [ ] ,
333+ userStats : null ,
303334 type : 'contacts'
304335 } ) ;
305336
@@ -364,12 +395,13 @@ export class PrimalCacheService {
364395 } , timeoutMs ) ;
365396
366397 this . pendingRequests . set ( requestId , {
367- resolve : resolve as ( value : PrimalSearchResult | PrimalFeedResponse | string [ ] ) => void ,
398+ resolve : ( value ) => resolve ( value as PrimalFeedResponse ) ,
368399 reject,
369400 timeout,
370401 profiles : [ ] ,
371402 events : [ ] ,
372403 follows : [ ] ,
404+ userStats : null ,
373405 type : 'feed'
374406 } ) ;
375407
@@ -427,12 +459,13 @@ export class PrimalCacheService {
427459 } , timeoutMs ) ;
428460
429461 this . pendingRequests . set ( requestId , {
430- resolve : resolve as ( value : PrimalSearchResult | PrimalFeedResponse | string [ ] ) => void ,
462+ resolve : ( value ) => resolve ( value as PrimalFeedResponse ) ,
431463 reject,
432464 timeout,
433465 profiles : [ ] ,
434466 events : [ ] ,
435467 follows : [ ] ,
468+ userStats : null ,
436469 type : 'global'
437470 } ) ;
438471
@@ -496,12 +529,13 @@ export class PrimalCacheService {
496529 } , timeoutMs ) ;
497530
498531 this . pendingRequests . set ( requestId , {
499- resolve : resolve as ( value : PrimalSearchResult | PrimalFeedResponse | string [ ] ) => void ,
532+ resolve : ( value ) => resolve ( value as PrimalFeedResponse ) ,
500533 reject,
501534 timeout,
502535 profiles : [ ] ,
503536 events : [ ] ,
504537 follows : [ ] ,
538+ userStats : null ,
505539 type : 'articles'
506540 } ) ;
507541
@@ -556,12 +590,13 @@ export class PrimalCacheService {
556590 } , timeoutMs ) ;
557591
558592 this . pendingRequests . set ( requestId , {
559- resolve : resolve as ( value : PrimalSearchResult | PrimalFeedResponse | string [ ] ) => void ,
593+ resolve : ( value ) => resolve ( value as PrimalFeedResponse ) ,
560594 reject,
561595 timeout,
562596 profiles : [ ] ,
563597 events : [ ] ,
564598 follows : [ ] ,
599+ userStats : null ,
565600 type : 'articles'
566601 } ) ;
567602
@@ -606,6 +641,7 @@ export class PrimalCacheService {
606641 profiles : [ ] ,
607642 events : [ ] ,
608643 follows : [ ] ,
644+ userStats : null ,
609645 type : 'search'
610646 } ) ;
611647
@@ -619,6 +655,57 @@ export class PrimalCacheService {
619655 } ) ;
620656 }
621657
658+ /**
659+ * Fetch user profile stats (follower count, following count, etc.) from Primal cache
660+ * Uses the user_profile cache endpoint which returns kind 10000105 events
661+ */
662+ public async fetchUserStats ( pubkey : string , timeoutMs : number = 5000 ) : Promise < PrimalUserStats | null > {
663+ if ( ! pubkey ) return null ;
664+
665+ if ( ! this . ws || this . ws . readyState !== WebSocket . OPEN ) {
666+ await this . connect ( ) ;
667+ }
668+
669+ if ( ! this . ws || this . ws . readyState !== WebSocket . OPEN ) {
670+ throw new Error ( 'WebSocket not connected' ) ;
671+ }
672+
673+ const requestId = this . generateRequestId ( ) ;
674+ const request = [
675+ 'REQ' ,
676+ requestId ,
677+ {
678+ cache : [ 'user_profile' , { pubkey } ]
679+ }
680+ ] ;
681+
682+ return new Promise ( ( resolve , reject ) => {
683+ const timeout = setTimeout ( ( ) => {
684+ this . pendingRequests . delete ( requestId ) ;
685+ reject ( new Error ( 'User stats request timed out' ) ) ;
686+ } , timeoutMs ) ;
687+
688+ this . pendingRequests . set ( requestId , {
689+ resolve : ( value ) => resolve ( value as PrimalUserStats | null ) ,
690+ reject,
691+ timeout,
692+ profiles : [ ] ,
693+ events : [ ] ,
694+ follows : [ ] ,
695+ userStats : null ,
696+ type : 'user_stats'
697+ } ) ;
698+
699+ try {
700+ this . ws ! . send ( JSON . stringify ( request ) ) ;
701+ } catch ( error ) {
702+ clearTimeout ( timeout ) ;
703+ this . pendingRequests . delete ( requestId ) ;
704+ reject ( error as Error ) ;
705+ }
706+ } ) ;
707+ }
708+
622709 public isConnected ( ) : boolean {
623710 return this . ws ?. readyState === WebSocket . OPEN ;
624711 }
@@ -641,6 +728,22 @@ export const getPrimalCache = (): PrimalCacheService | null => {
641728 return primalCache ;
642729} ;
643730
731+ /**
732+ * Fetch user profile stats (followers, following, etc.) from Primal cache
733+ * Convenience wrapper that handles connection and errors
734+ */
735+ export async function fetchUserStatsFromPrimal ( pubkey : string ) : Promise < PrimalUserStats | null > {
736+ const cache = getPrimalCache ( ) ;
737+ if ( ! cache ) return null ;
738+
739+ try {
740+ return await cache . fetchUserStats ( pubkey ) ;
741+ } catch ( error ) {
742+ console . debug ( '[PrimalCache] Failed to fetch user stats:' , error ) ;
743+ return null ;
744+ }
745+ }
746+
644747// ═══════════════════════════════════════════════════════════════
645748// CONVENIENCE FUNCTIONS FOR FEED INTEGRATION
646749// ═══════════════════════════════════════════════════════════════
0 commit comments