77 *
88 * @copyright Copyright (c) 2023 Distributive Corp.
99 */
10+ 'use strict' ;
1011
1112const { EventTarget, Event } = require ( 'event-target' ) ;
1213const { DOMException } = require ( 'dom-exception' ) ;
1314const { URL , URLSearchParams } = require ( 'url' ) ;
1415const { request, decodeStr } = require ( 'XMLHttpRequest-internal' ) ;
16+ const debug = globalThis . python . eval ( '__import__("pythonmonkey").bootstrap.require' ) ( 'debug' ) ;
17+
18+ /**
19+ * Truncate a string-like thing for display purposes, returning a string.
20+ * @param {any } what The thing to truncate; must have a slice method and index property.
21+ * Works with string, array, typedarray, etc.
22+ * @param {number } maxlen The maximum length for truncation
23+ * @param {boolean } coerce Not false = coerce to printable character codes
24+ * @returns {string }
25+ */
26+ function trunc ( what , maxlen , coerce )
27+ {
28+ if ( coerce !== false && typeof what !== 'string' )
29+ {
30+ what = Array . from ( what ) . map ( x => {
31+ if ( x > 31 && x < 127 )
32+ return String . fromCharCode ( x ) ;
33+ else if ( x < 32 )
34+ return String . fromCharCode ( 0x2400 + Number ( x ) ) ;
35+ else if ( x === 127 )
36+ return '\u2421' ;
37+ else
38+ return '\u2423' ;
39+ } ) . join ( '' ) ;
40+ }
41+ return `${ what . slice ( 0 , maxlen ) } ${ what . length > maxlen ? '\u2026' : '' } ` ;
42+ }
1543
1644// exposed
1745/**
@@ -29,6 +57,7 @@ class ProgressEvent extends Event
2957 this . lengthComputable = eventInitDict . lengthComputable ?? false ;
3058 this . loaded = eventInitDict . loaded ?? 0 ;
3159 this . total = eventInitDict . total ?? 0 ;
60+ this . debugTag = 'xhr:' ;
3261 }
3362}
3463
@@ -112,6 +141,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
112141 */
113142 open ( method , url , async = true , username = null , password = null )
114143 {
144+ debug ( 'xhr:open' ) ( 'open start, method=' + method ) ;
115145 // Normalize the method.
116146 // @ts -expect-error
117147 method = method . toString ( ) . toUpperCase ( ) ;
@@ -125,7 +155,8 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
125155 parsedURL . username = username ;
126156 if ( password )
127157 parsedURL . password = password ;
128-
158+ debug ( 'xhr:open' ) ( 'url is ' + parsedURL . href ) ;
159+
129160 // step 11
130161 this . #sendFlag = false ;
131162 this . #uploadListenerFlag = false ;
@@ -144,6 +175,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
144175 this . #state = XMLHttpRequest . OPENED ;
145176 this . dispatchEvent ( new Event ( 'readystatechange' ) ) ;
146177 }
178+ debug ( 'xhr:open' ) ( 'finished open, state is ' + this . #state) ;
147179 }
148180
149181 /**
@@ -153,6 +185,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
153185 */
154186 setRequestHeader ( name , value )
155187 {
188+ debug ( 'xhr:headers' ) ( `set header ${ name } =${ value } ` ) ;
156189 if ( this . #state !== XMLHttpRequest . OPENED )
157190 throw new DOMException ( 'setRequestHeader can only be called when state is OPEN' , 'InvalidStateError' ) ;
158191 if ( this . #sendFlag)
@@ -218,6 +251,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
218251 */
219252 send ( body = null )
220253 {
254+ debug ( 'xhr:send' ) ( `sending; body length=${ body ?. length } ` ) ;
221255 if ( this . #state !== XMLHttpRequest . OPENED ) // step 1
222256 throw new DOMException ( 'connection must be opened before send() is called' , 'InvalidStateError' ) ;
223257 if ( this . #sendFlag) // step 2
@@ -248,10 +282,9 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
248282
249283 const originalAuthorContentType = this . #requestHeaders[ 'content-type' ] ;
250284 if ( ! originalAuthorContentType && extractedContentType )
251- {
252285 this . #requestHeaders[ 'content-type' ] = extractedContentType ;
253- }
254286 }
287+ debug ( 'xhr:send' ) ( `content-type=${ this . #requestHeaders[ 'content-type' ] } ` ) ;
255288
256289 // step 5
257290 if ( this . #uploadObject. _hasAnyListeners ( ) )
@@ -276,6 +309,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
276309 */
277310 #sendAsync( )
278311 {
312+ debug ( 'xhr:send' ) ( 'sending in async mode' ) ;
279313 this . dispatchEvent ( new ProgressEvent ( 'loadstart' , { loaded :0 , total :0 } ) ) ; // step 11.1
280314
281315 let requestBodyTransmitted = 0 ; // step 11.2
@@ -308,6 +342,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
308342 let responseLength = 0 ;
309343 const processResponse = ( response ) =>
310344 {
345+ debug ( 'xhr:response' ) ( `response headers ----\n${ response . getAllResponseHeaders ( ) } ` ) ;
311346 this . #response = response ; // step 11.9.1
312347 this . #state = XMLHttpRequest . HEADERS_RECEIVED ; // step 11.9.4
313348 this . dispatchEvent ( new Event ( 'readystatechange' ) ) ; // step 11.9.5
@@ -318,6 +353,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
318353
319354 const processBodyChunk = ( /** @type {Uint8Array } */ bytes ) =>
320355 {
356+ debug ( 'xhr:response' ) ( `recv chunk, ${ bytes . length } bytes (${ trunc ( bytes , 100 ) } )` ) ;
321357 this . #receivedBytes. push ( bytes ) ;
322358 if ( this . #state === XMLHttpRequest . HEADERS_RECEIVED )
323359 this . #state = XMLHttpRequest . LOADING ;
@@ -330,16 +366,22 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
330366 */
331367 const processEndOfBody = ( ) =>
332368 {
369+ debug ( 'xhr:response' ) ( `end of body, received ${ this . #receivedLength} bytes` ) ;
333370 const transmitted = this . #receivedLength; // step 3
334371 const length = responseLength || 0 ; // step 4
372+
335373 this . dispatchEvent ( new ProgressEvent ( 'progress' , { loaded :transmitted , total :length } ) ) ; // step 6
336374 this . #state = XMLHttpRequest . DONE ; // step 7
337375 this . #sendFlag = false ; // step 8
376+
338377 this . dispatchEvent ( new Event ( 'readystatechange' ) ) ; // step 9
339378 for ( const eventType of [ 'load' , 'loadend' ] ) // step 10, step 11
340379 this . dispatchEvent ( new ProgressEvent ( eventType , { loaded :transmitted , total :length } ) ) ;
341380 } ;
342381
382+ debug ( 'xhr:send' ) ( `${ this . #requestMethod} ${ this . #requestURL. href } ` ) ;
383+ debug ( 'xhr:headers' ) ( 'headers=' + Object . entries ( this . #requestHeaders) ) ;
384+
343385 // send() step 6
344386 request (
345387 this . #requestMethod,
@@ -362,8 +404,8 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
362404 */
363405 #sendSync( )
364406 {
407+ /* Synchronous XHR deprecated. /wg march 2024 */
365408 throw new DOMException ( 'synchronous XHR is not supported' , 'NotSupportedError' ) ;
366- // TODO: handle synchronous request
367409 }
368410
369411 /**
@@ -376,7 +418,6 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
376418 return ;
377419 if ( this . #timedOutFlag) // step 2
378420 return this . #reportRequestError( 'timeout' , new DOMException ( e . toString ( ) , 'TimeoutError' ) ) ;
379- console . error ( e ) ; // similar to browsers, print out network errors even then the error will be handled by `xhr.onerror`
380421 if ( this . #response === null /* network error */ ) // step 4
381422 return this . #reportRequestError( 'error' , new DOMException ( e . toString ( ) , 'NetworkError' ) ) ;
382423 else // unknown errors
@@ -652,6 +693,10 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
652693 }
653694}
654695
696+ /* A side-effect of loading this module is to add the XMLHttpRequest and related symbols to the global
697+ * object. This makes them accessible in the "normal" way (like in a browser) even in PythonMonkey JS
698+ * host environments which don't include a require() symbol.
699+ */
655700if ( ! globalThis . XMLHttpRequestEventTarget )
656701 globalThis . XMLHttpRequestEventTarget = XMLHttpRequestEventTarget ;
657702if ( ! globalThis . XMLHttpRequestUpload )
0 commit comments