@@ -740,4 +740,152 @@ export function validateCoordinates(
740740 valid : true ,
741741 coords : { top, bottom, left, right }
742742 } ;
743+ }
744+
745+ export function createBasicAuthToken ( username : string , accessKey : string ) : string {
746+ const credentials = `${ username } :${ accessKey } ` ;
747+ return Buffer . from ( credentials ) . toString ( 'base64' ) ;
748+ }
749+
750+ export async function listenToSmartUISSE (
751+ baseURL : string ,
752+ accessToken : string ,
753+ ctx : Context ,
754+ onEvent ?: ( eventType : string , data : any ) => void
755+ ) : Promise < { abort : ( ) => void } > {
756+ const url = `${ baseURL } /api/v1/sse/smartui` ;
757+
758+ const abortController = new AbortController ( ) ;
759+
760+ try {
761+ const response = await fetch ( url , {
762+ method : 'GET' ,
763+ headers : {
764+ 'Accept' : 'text/event-stream' ,
765+ 'Cache-Control' : 'no-cache' ,
766+ 'Cookie' : `stageAccessToken=Basic ${ accessToken } `
767+ } ,
768+ signal : abortController . signal
769+ } ) ;
770+
771+ if ( ! response . ok ) {
772+ throw new Error ( `HTTP error! status: ${ response . status } ` ) ;
773+ }
774+
775+ onEvent ?.( 'open' , { status : 'connected' } ) ;
776+
777+ const reader = response . body ?. getReader ( ) ;
778+ if ( ! reader ) {
779+ throw new Error ( 'No response body reader available' ) ;
780+ }
781+
782+ const decoder = new TextDecoder ( ) ;
783+ let buffer = '' ;
784+ let currentEvent = '' ;
785+
786+ try {
787+ while ( true ) {
788+ const { done, value } = await reader . read ( ) ;
789+ if ( done ) break ;
790+
791+ const chunk = decoder . decode ( value , { stream : true } ) ;
792+
793+ buffer += chunk ;
794+ const lines = buffer . split ( '\n' ) ;
795+
796+ buffer = lines . pop ( ) || '' ;
797+
798+ for ( const line of lines ) {
799+ if ( line . startsWith ( 'event:' ) ) {
800+ currentEvent = line . substring ( 6 ) . trim ( ) ;
801+ }
802+ else if ( line . startsWith ( 'data:' ) ) {
803+ const data = line . substring ( 5 ) . trim ( ) ;
804+
805+ if ( data ) {
806+ try {
807+ const parsedData = JSON . parse ( data ) ;
808+ onEvent ?.( currentEvent , parsedData ) ;
809+ } catch ( parseError ) {
810+ if ( currentEvent === 'connection' && data === 'connected' ) {
811+ onEvent ?.( currentEvent , { status : 'connected' , message : data } ) ;
812+ } else {
813+ onEvent ?.( currentEvent , data ) ;
814+ }
815+ }
816+ }
817+ }
818+ else if ( line . trim ( ) === '' ) {
819+ currentEvent = '' ;
820+ }
821+ }
822+ }
823+ } catch ( streamError : any ) {
824+ ctx . log . debug ( 'SSE Streaming error:' , streamError ) ;
825+ onEvent ?.( 'error' , streamError ) ;
826+ } finally {
827+ reader . releaseLock ( ) ;
828+ }
829+
830+ } catch ( error ) {
831+ ctx . log . debug ( 'SSE Connection error:' , error ) ;
832+ onEvent ?.( 'error' , error ) ;
833+ }
834+
835+ return {
836+ abort : ( ) => abortController . abort ( )
837+ } ;
838+ }
839+
840+ export async function startSSEListener ( ctx : Context ) {
841+ let currentConnection : { abort : ( ) => void } | null = null ;
842+ let errorCount = 0 ;
843+
844+ try {
845+ ctx . log . debug ( 'Attempting SSE connection' ) ;
846+ const accessKey = ctx . env . LT_ACCESS_KEY ;
847+ const username = ctx . env . LT_USERNAME ;
848+
849+ const basicAuthToken = createBasicAuthToken ( username , accessKey ) ;
850+ ctx . log . debug ( `Basic auth token: ${ basicAuthToken } ` ) ;
851+ currentConnection = await listenToSmartUISSE (
852+ ctx . env . SMARTUI_SSE_URL ,
853+ basicAuthToken ,
854+ ctx ,
855+ ( eventType , data ) => {
856+ switch ( eventType ) {
857+ case 'open' :
858+ ctx . log . debug ( 'Connected to SSE server' ) ;
859+ break ;
860+
861+ case 'connection' :
862+ ctx . log . debug ( 'Connection confirmed:' , data ) ;
863+ break ;
864+
865+ case 'Dot_buildCompleted' :
866+ ctx . log . debug ( 'Build completed' ) ;
867+ ctx . log . info ( chalk . green . bold ( 'Build completed' ) ) ;
868+ process . exit ( 0 ) ;
869+ case 'DOTUIError' :
870+ if ( data . buildId == ctx . build . id ) {
871+ errorCount ++ ;
872+ ctx . log . info ( chalk . red . bold ( `Error: ${ data . message } ` ) ) ;
873+ }
874+ break ;
875+ case 'DOTUIWarning' :
876+ if ( data . buildId == ctx . build . id ) {
877+ ctx . log . info ( chalk . yellow . bold ( `Warning: ${ data . message } ` ) ) ;
878+ }
879+ break ;
880+ case 'error' :
881+ ctx . log . debug ( 'SSE Error occurred:' , data ) ;
882+ currentConnection ?. abort ( ) ;
883+ return ;
884+ }
885+ }
886+ ) ;
887+
888+ } catch ( error ) {
889+ ctx . log . debug ( 'Failed to start SSE listener:' , error ) ;
890+ }
743891}
0 commit comments