@@ -24,11 +24,74 @@ export interface Beneficiary {
2424
2525const STORAGE_KEY = 'stellar_beneficiaries' ;
2626
27+ /**
28+ * Request deduplication system to prevent duplicate API calls.
29+ * Concurrent fetches for the same key share one in-flight promise,
30+ * with the result distributed to all waiters.
31+ */
32+ class RequestDeduplicator {
33+ private inFlightRequests = new Map < string , Promise < Beneficiary [ ] > > ( ) ;
34+
35+ /**
36+ * Get or create a promise for the given key.
37+ * If a request is already in-flight, return the existing promise.
38+ */
39+ async fetch < T > (
40+ key : string ,
41+ fetchFn : ( ) => Promise < T > ,
42+ ) : Promise < T > {
43+ // Check if a request is already in-flight
44+ if ( this . inFlightRequests . has ( key ) ) {
45+ return this . inFlightRequests . get ( key ) as Promise < T > ;
46+ }
47+
48+ // Create new promise and track it
49+ const promise = ( async ( ) => {
50+ try {
51+ const result = await fetchFn ( ) ;
52+ return result ;
53+ } finally {
54+ // Remove from tracking after completion (success or failure)
55+ this . inFlightRequests . delete ( key ) ;
56+ }
57+ } ) ( ) ;
58+
59+ this . inFlightRequests . set ( key , promise as Promise < Beneficiary [ ] > ) ;
60+ return promise ;
61+ }
62+ }
63+
64+ const deduplicator = new RequestDeduplicator ( ) ;
65+
2766function generateId ( ) : string {
2867 return `ben_${ Date . now ( ) } _${ Math . random ( ) . toString ( 36 ) . substr ( 2 , 9 ) } ` ;
2968}
3069
31- export function useBeneficiaries ( ) {
70+ /**
71+ * Fetch beneficiaries from the API with request deduplication.
72+ * All concurrent requests for the same key will share the same in-flight promise.
73+ */
74+ async function fetchBeneficiariesWithDedup (
75+ userId : string = 'current' ,
76+ ) : Promise < Beneficiary [ ] > {
77+ return deduplicator . fetch ( `beneficiaries:${ userId } ` , async ( ) => {
78+ try {
79+ const response = await fetch ( `/api/beneficiaries?userId=${ encodeURIComponent ( userId ) } ` ) ;
80+ if ( ! response . ok ) {
81+ throw new Error ( `HTTP ${ response . status } ` ) ;
82+ }
83+ const data = await response . json ( ) ;
84+ return Array . isArray ( data ) ? data : [ ] ;
85+ } catch ( error ) {
86+ console . error ( 'Failed to fetch beneficiaries:' , error ) ;
87+ // Return empty array on error to allow graceful fallback
88+ return [ ] ;
89+ }
90+ } ) ;
91+ }
92+
93+ export function useBeneficiaries ( options ?: { fetchFromApi ?: boolean ; userId ?: string } ) {
94+ const { fetchFromApi = false , userId = 'current' } = options || { } ;
3295 const [ beneficiaries , setBeneficiaries ] = useState < Beneficiary [ ] > ( [ ] ) ;
3396 const [ isLoaded , setIsLoaded ] = useState ( false ) ;
3497 const [ selectedIndex , setSelectedIndex ] = useState < number > ( - 1 ) ;
@@ -38,8 +101,35 @@ export function useBeneficiaries() {
38101 setIsMounted ( true ) ;
39102 } , [ ] ) ;
40103
104+ // Fetch from API with deduplication if enabled
105+ useEffect ( ( ) => {
106+ if ( ! isMounted || ! fetchFromApi || typeof window === 'undefined' ) return ;
107+
108+ let cancelled = false ;
109+
110+ ( async ( ) => {
111+ try {
112+ const data = await fetchBeneficiariesWithDedup ( userId ) ;
113+ if ( ! cancelled ) {
114+ setBeneficiaries ( Array . isArray ( data ) ? data : [ ] ) ;
115+ setIsLoaded ( true ) ;
116+ }
117+ } catch ( error ) {
118+ console . error ( 'Error loading beneficiaries:' , error ) ;
119+ if ( ! cancelled ) {
120+ setIsLoaded ( true ) ;
121+ }
122+ }
123+ } ) ( ) ;
124+
125+ return ( ) => {
126+ cancelled = true ;
127+ } ;
128+ } , [ isMounted , fetchFromApi , userId ] ) ;
129+
130+ // Load from localStorage if not fetching from API
41131 useEffect ( ( ) => {
42- if ( ! isMounted || typeof window === 'undefined' ) return ;
132+ if ( ! isMounted || fetchFromApi || typeof window === 'undefined' ) return ;
43133 try {
44134 const stored = localStorage . getItem ( STORAGE_KEY ) ;
45135 if ( stored ) {
@@ -50,7 +140,7 @@ export function useBeneficiaries() {
50140 setBeneficiaries ( [ ] ) ;
51141 }
52142 setIsLoaded ( true ) ;
53- } , [ isMounted ] ) ;
143+ } , [ isMounted , fetchFromApi ] ) ;
54144
55145 useEffect ( ( ) => {
56146 if ( ! isLoaded || typeof window === 'undefined' ) return ;
0 commit comments