@@ -4,10 +4,12 @@ import {
44 Post ,
55 Body ,
66 Param ,
7+ Query ,
78 Headers ,
89 Req ,
910 HttpCode ,
1011 HttpException ,
12+ HttpStatus ,
1113 UnauthorizedException ,
1214} from '@nestjs/common' ;
1315import { ApiTags } from '@nestjs/swagger' ;
@@ -27,6 +29,14 @@ import {
2729 DeadLetterBatchControlRequest ,
2830 DeadLetterBatchControlResult ,
2931} from './types' ;
32+ import {
33+ StreamEnvelope ,
34+ StreamEnvelopeVersion ,
35+ StreamResumeTokenPayload ,
36+ StreamBackfillResponse ,
37+ StreamReconciliationRequirement ,
38+ } from './stream.types' ;
39+ import { StreamResumeService } from './stream-resume.service' ;
3040import { WebhookSignatureService } from './webhook-signature.service' ;
3141
3242/**
@@ -47,6 +57,7 @@ export class BlockchainController {
4757 private reconciliation : ReconciliationService ,
4858 private stateService : BlockchainStateService ,
4959 private voteResolver : WeightedVoteResolutionService ,
60+ private streamResume : StreamResumeService ,
5061 private readinessService : ReadinessService ,
5162 private replayService : DeadLetterReplayService ,
5263 private webhookSignatureService : WebhookSignatureService ,
@@ -294,6 +305,94 @@ export class BlockchainController {
294305 } ;
295306 }
296307
308+ /**
309+ * Get the current stream envelope metadata and a fresh resume token.
310+ * GET /api/v1/blockchain/streams/envelope
311+ */
312+ @Get ( 'streams/envelope' )
313+ async getStreamEnvelope ( ) : Promise < StreamEnvelope > {
314+ return this . streamResume . getCurrentEnvelope ( ) ;
315+ }
316+
317+ /**
318+ * Resume a stream from an opaque resume token.
319+ * POST /api/v1/blockchain/streams/resume
320+ */
321+ @Post ( 'streams/resume' )
322+ async resumeStream (
323+ @Body ( ) body : { resumeToken : string } ,
324+ ) : Promise < StreamBackfillResponse > {
325+ if ( ! body || typeof body . resumeToken !== 'string' || body . resumeToken . length === 0 ) {
326+ throw new HttpException (
327+ {
328+ error : 'invalid_resume_token' ,
329+ message : 'resumeToken is required' ,
330+ } ,
331+ HttpStatus . BAD_REQUEST ,
332+ ) ;
333+ }
334+
335+ const result = await this . streamResume . resume ( body . resumeToken ) ;
336+ if ( ! result . ok ) {
337+ throw new HttpException ( result . body , result . status ) ;
338+ }
339+ return result . body ;
340+ }
341+
342+ /**
343+ * Bounded HTTP backfill from a cursor.
344+ * GET /api/v1/blockchain/streams/backfill?cursor=...&limit=...
345+ */
346+ @Get ( 'streams/backfill' )
347+ async backfillStream (
348+ @Query ( 'cursor' ) cursor : string ,
349+ @Query ( 'limit' ) limit ?: string ,
350+ @Headers ( 'if-none-match' ) etag ?: string ,
351+ ) : Promise < StreamBackfillResponse > {
352+ if ( ! cursor || cursor . length === 0 ) {
353+ throw new HttpException (
354+ { error : 'invalid_cursor' , message : 'cursor is required' } ,
355+ HttpStatus . BAD_REQUEST ,
356+ ) ;
357+ }
358+
359+ const parsedLimit = limit === undefined ? undefined : Number ( limit ) ;
360+ if (
361+ parsedLimit !== undefined &&
362+ ( ! Number . isFinite ( parsedLimit ) || parsedLimit <= 0 )
363+ ) {
364+ throw new HttpException (
365+ { error : 'invalid_limit' , message : 'limit must be a positive integer' } ,
366+ HttpStatus . BAD_REQUEST ,
367+ ) ;
368+ }
369+
370+ const result = await this . streamResume . backfill ( cursor , parsedLimit , etag ) ;
371+ if ( ! result . ok ) {
372+ throw new HttpException ( result . body , result . status ) ;
373+ }
374+ return result . body ;
375+ }
376+
377+ /**
378+ * Decode a resume token for observability / client reconciliation.
379+ * Tokens are opaque to clients but can be inspected to determine whether a
380+ * canonical resync is required. No protected payload is returned.
381+ * GET /api/v1/blockchain/streams/token/inspect?token=...
382+ */
383+ @Get ( 'streams/token/inspect' )
384+ async inspectResumeToken (
385+ @Query ( 'token' ) token : string ,
386+ ) : Promise < StreamReconciliationRequirement > {
387+ if ( ! token || token . length === 0 ) {
388+ throw new HttpException (
389+ { error : 'invalid_resume_token' , message : 'token is required' } ,
390+ HttpStatus . BAD_REQUEST ,
391+ ) ;
392+ }
393+ return this . streamResume . inspectToken ( token ) ;
394+ }
395+
297396 /**
298397 * Replay a single dead-letter message.
299398 *
@@ -422,7 +521,7 @@ export class BlockchainController {
422521 {
423522 ...baseVote ,
424523 userId : 'user2' ,
425- verdict : 'FALSE ' ,
524+ verdict : 'ALSE ' ,
426525 userReputation : 70 ,
427526 stakeAmount : '100' ,
428527 } ,
0 commit comments