@@ -11,6 +11,7 @@ import { updateUserLocationIfChanged } from './utils/location.js';
1111const activeRequests = new Map ( ) ;
1212let gameJoinErrorCount = 0 ;
1313let lastGameJoinRequestTime = 0 ;
14+ const GAMEJOIN_TIMEOUT_MS = 2000 ;
1415
1516const OAUTH_STORAGE_KEY = 'rovalra_oauth_verification' ;
1617let cachedRovalraUserAgent = null ;
@@ -72,6 +73,103 @@ function getRequestKey({
7273 return `${ target } |${ method . toUpperCase ( ) } |${ bodyStr } |${ headersStr } ` ;
7374}
7475
76+ function normalizeGameJoinEndpoint ( endpoint ) {
77+ if ( typeof endpoint !== 'string' ) return endpoint ;
78+ return endpoint . replace ( / ^ \/ v 1 \/ / , '/v2/' ) ;
79+ }
80+
81+ function isGameJoinTimeoutEnabled ( endpoint ) {
82+ if ( typeof endpoint !== 'string' ) return true ;
83+ return endpoint . split ( '?' ) [ 0 ] !== '/v2/join-game' ;
84+ }
85+
86+ function createGameJoinFullResponse ( ) {
87+ return new Response (
88+ JSON . stringify ( {
89+ status : 22 ,
90+ message : 'Server full' ,
91+ rovalraTimedOut : true ,
92+ } ) ,
93+ {
94+ status : 200 ,
95+ headers : { 'Content-Type' : 'application/json' } ,
96+ } ,
97+ ) ;
98+ }
99+
100+ function parseServerSentEvents ( text ) {
101+ const events = [ ] ;
102+ const blocks = String ( text || '' ) . split ( / \r ? \n \r ? \n / ) ;
103+
104+ for ( const block of blocks ) {
105+ if ( ! block . trim ( ) ) continue ;
106+
107+ const event = { event : 'message' , data : '' } ;
108+ const dataLines = [ ] ;
109+
110+ for ( const rawLine of block . split ( / \r ? \n / ) ) {
111+ if ( ! rawLine || rawLine . startsWith ( ':' ) ) continue ;
112+
113+ const separatorIndex = rawLine . indexOf ( ':' ) ;
114+ let field = rawLine ;
115+ let value = '' ;
116+
117+ if ( separatorIndex !== - 1 ) {
118+ field = rawLine . slice ( 0 , separatorIndex ) ;
119+ value = rawLine . slice ( separatorIndex + 1 ) ;
120+ if ( value . charCodeAt ( 0 ) === 32 ) value = value . slice ( 1 ) ;
121+ }
122+
123+ if ( field === 'event' ) event . event = value ;
124+ else if ( field === 'id' ) event . id = value ;
125+ else if ( field === 'retry' ) event . retry = value ;
126+ else if ( field === 'data' ) dataLines . push ( value ) ;
127+ }
128+
129+ event . data = dataLines . join ( '\n' ) ;
130+ events . push ( event ) ;
131+ }
132+
133+ return events ;
134+ }
135+
136+ async function normalizeGameJoinResponse ( response ) {
137+ const contentType = (
138+ response . headers . get ( 'Content-Type' ) || ''
139+ ) . toLowerCase ( ) ;
140+ if ( ! contentType . includes ( 'text/event-stream' ) ) return response ;
141+
142+ const text = await response . text ( ) ;
143+ const events = parseServerSentEvents ( text ) ;
144+ const readyEvent =
145+ events . find ( ( event ) => event . event === 'ResponseReady' ) ||
146+ events . find ( ( event ) => event . data ?. trim ( ) ) ;
147+
148+ if ( ! readyEvent ?. data ) {
149+ return new Response ( JSON . stringify ( { status : 0 } ) , {
150+ status : response . status ,
151+ statusText : response . statusText ,
152+ headers : { 'Content-Type' : 'application/json' } ,
153+ } ) ;
154+ }
155+
156+ try {
157+ JSON . parse ( readyEvent . data ) ;
158+ } catch ( e ) {
159+ return new Response ( JSON . stringify ( { status : 0 } ) , {
160+ status : response . status ,
161+ statusText : response . statusText ,
162+ headers : { 'Content-Type' : 'application/json' } ,
163+ } ) ;
164+ }
165+
166+ return new Response ( readyEvent . data , {
167+ status : response . status ,
168+ statusText : response . statusText ,
169+ headers : { 'Content-Type' : 'application/json' } ,
170+ } ) ;
171+ }
172+
75173function checkSimulatedDowntime ( ) {
76174 return new Promise ( ( resolve ) => {
77175 if (
@@ -141,6 +239,13 @@ export function resetGameJoinErrorCount() {
141239}
142240
143241export async function callRobloxApi ( options ) {
242+ if ( options . subdomain === 'gamejoin' ) {
243+ options = {
244+ ...options ,
245+ endpoint : normalizeGameJoinEndpoint ( options . endpoint ) ,
246+ } ;
247+ }
248+
144249 if (
145250 options . subdomain === 'gamejoin' &&
146251 ( options . method || 'GET' ) . toUpperCase ( ) === 'POST'
@@ -541,10 +646,52 @@ export async function callRobloxApi(options) {
541646 }
542647 }
543648
649+ let timeoutId = null ;
650+ let abortSignalCleanup = null ;
651+ let didGameJoinTimeout = false ;
652+ const shouldUseGameJoinTimeout =
653+ subdomain === 'gamejoin' && isGameJoinTimeoutEnabled ( endpoint ) ;
654+
655+ if ( shouldUseGameJoinTimeout ) {
656+ const controller = new AbortController ( ) ;
657+ timeoutId = setTimeout ( ( ) => {
658+ didGameJoinTimeout = true ;
659+ controller . abort ( ) ;
660+ } , GAMEJOIN_TIMEOUT_MS ) ;
661+
662+ if ( signal ) {
663+ if ( signal . aborted ) {
664+ controller . abort ( ) ;
665+ } else {
666+ abortSignalCleanup = ( ) => controller . abort ( ) ;
667+ signal . addEventListener ( 'abort' , abortSignalCleanup , {
668+ once : true ,
669+ } ) ;
670+ }
671+ }
672+
673+ fetchOptions . signal = controller . signal ;
674+ }
675+
676+ const cleanupGameJoinTimeout = ( ) => {
677+ if ( timeoutId ) {
678+ clearTimeout ( timeoutId ) ;
679+ timeoutId = null ;
680+ }
681+ if ( signal && abortSignalCleanup ) {
682+ signal . removeEventListener ( 'abort' , abortSignalCleanup ) ;
683+ abortSignalCleanup = null ;
684+ }
685+ } ;
686+
544687 let response ;
545688 try {
546689 response = await fetch ( fullUrl , fetchOptions ) ;
547690 } catch ( error ) {
691+ cleanupGameJoinTimeout ( ) ;
692+ if ( didGameJoinTimeout ) {
693+ return createGameJoinFullResponse ( ) ;
694+ }
548695 if ( error . name === 'AbortError' || ( signal && signal . aborted ) ) {
549696 return new Response ( null , {
550697 status : 499 ,
@@ -563,6 +710,10 @@ export async function callRobloxApi(options) {
563710 try {
564711 response = await fetch ( fullUrl , fetchOptions ) ;
565712 } catch ( error ) {
713+ cleanupGameJoinTimeout ( ) ;
714+ if ( didGameJoinTimeout ) {
715+ return createGameJoinFullResponse ( ) ;
716+ }
566717 if (
567718 error . name === 'AbortError' ||
568719 ( signal && signal . aborted )
@@ -577,6 +728,19 @@ export async function callRobloxApi(options) {
577728 }
578729 }
579730
731+ try {
732+ if ( subdomain === 'gamejoin' ) {
733+ response = await normalizeGameJoinResponse ( response ) ;
734+ }
735+ } catch ( error ) {
736+ if ( didGameJoinTimeout ) {
737+ return createGameJoinFullResponse ( ) ;
738+ }
739+ throw error ;
740+ } finally {
741+ cleanupGameJoinTimeout ( ) ;
742+ }
743+
580744 if ( ! response . ok ) {
581745 console . error (
582746 `RoValra API: Request to ${ fullUrl } failed with status ${ response . status } .` ,
0 commit comments