@@ -10,6 +10,8 @@ import {
1010 HttpCode ,
1111 HttpStatus ,
1212 Body ,
13+ Res ,
14+ BadRequestException ,
1315} from '@nestjs/common' ;
1416import {
1517 ApiTags ,
@@ -19,6 +21,7 @@ import {
1921 ApiQuery ,
2022} from '@nestjs/swagger' ;
2123import { Throttle } from '@nestjs/throttler' ;
24+ import type { Response } from 'express' ;
2225import { ClaimsService } from './claims.service' ;
2326import { ClaimsListResponseDto , ClaimDetailResponseDto } from './dto/claim.dto' ;
2427import { BuildClaimTransactionDto } from './dto/build-claim-transaction.dto' ;
@@ -28,6 +31,9 @@ import { WalletAddress } from '../auth/decorators/wallet-address.decorator';
2831import { RateLimitGuard } from '../rate-limit/rate-limit.guard' ;
2932import { MAX_LIMIT , DEFAULT_LIMIT } from '../helpers/pagination' ;
3033
34+ /** Maximum claim IDs accepted per status-poll or SSE subscription. */
35+ const MAX_WATCH_IDS = 50 ;
36+
3137@ApiTags ( 'claims' )
3238@Controller ( 'claims' )
3339export class ClaimsController {
@@ -122,4 +128,65 @@ export class ClaimsController {
122128 async submitTransaction ( @Body ( ) dto : SubmitTransactionDto ) {
123129 return this . claimsService . submitTransaction ( dto . transactionXdr ) ;
124130 }
125- }
131+
132+ // ── Claim status polling (for watched claims) ────────────────────────────
133+
134+ /**
135+ * GET /api/claims/status?claimId=1&claimId=2
136+ * Returns the current status for up to MAX_WATCH_IDS claim IDs.
137+ * Used by the frontend polling loop (useClaimWatcher).
138+ * Latency: indexer lag + cache TTL, typically < 30 s on Mainnet.
139+ */
140+ @Get ( 'status' )
141+ @Throttle ( { default : { limit : 30 , ttl : 60_000 } } )
142+ @ApiOperation ( { summary : 'Poll current status for a set of watched claim IDs' } )
143+ @ApiQuery ( { name : 'claimId' , required : true , isArray : true , type : String } )
144+ @ApiResponse ( { status : 200 , description : 'Array of { claimId, status, updatedAt }' } )
145+ async getClaimStatuses (
146+ @Query ( 'claimId' ) claimId : string | string [ ] ,
147+ ) : Promise < { claimId : string ; status : string ; updatedAt : string } [ ] > {
148+ const ids = ( Array . isArray ( claimId ) ? claimId : [ claimId ] ) . slice ( 0 , MAX_WATCH_IDS ) ;
149+ if ( ids . length === 0 ) throw new BadRequestException ( 'At least one claimId is required.' ) ;
150+ return this . claimsService . getClaimStatuses ( ids ) ;
151+ }
152+
153+ // ── SSE stream for claim status changes ──────────────────────────────────
154+
155+ /**
156+ * GET /api/claims/status/stream?claimId=1&claimId=2
157+ * Server-Sent Events stream that pushes status-change events for watched claims.
158+ * Falls back gracefully — clients use polling if SSE is unavailable.
159+ * Max latency: indexer lag + push delay, typically < 15 s on Mainnet.
160+ */
161+ @Get ( 'status/stream' )
162+ @Throttle ( { default : { limit : 5 , ttl : 60_000 } } )
163+ @ApiOperation ( { summary : 'SSE stream for watched claim status changes' } )
164+ @ApiQuery ( { name : 'claimId' , required : true , isArray : true , type : String } )
165+ @ApiResponse ( { status : 200 , description : 'text/event-stream' } )
166+ streamClaimStatuses (
167+ @Query ( 'claimId' ) claimId : string | string [ ] ,
168+ @Res ( ) res : Response ,
169+ ) : void {
170+ const ids = ( Array . isArray ( claimId ) ? claimId : [ claimId ] ) . slice ( 0 , MAX_WATCH_IDS ) ;
171+
172+ res . setHeader ( 'Content-Type' , 'text/event-stream' ) ;
173+ res . setHeader ( 'Cache-Control' , 'no-cache' ) ;
174+ res . setHeader ( 'Connection' , 'keep-alive' ) ;
175+ res . setHeader ( 'X-Accel-Buffering' , 'no' ) ; // disable nginx buffering
176+ res . flushHeaders ( ) ;
177+
178+ const send = ( data : object ) => {
179+ res . write ( `data: ${ JSON . stringify ( data ) } \n\n` ) ;
180+ } ;
181+
182+ // Send a heartbeat every 25 s to keep the connection alive through proxies.
183+ const heartbeat = setInterval ( ( ) => res . write ( ': heartbeat\n\n' ) , 25_000 ) ;
184+
185+ // Subscribe to status changes for the requested claim IDs.
186+ const unsubscribe = this . claimsService . subscribeToStatusChanges ( ids , send ) ;
187+
188+ res . on ( 'close' , ( ) => {
189+ clearInterval ( heartbeat ) ;
190+ unsubscribe ( ) ;
191+ } ) ;
192+ }
0 commit comments