7
7
*
8
8
* @copyright Copyright (c) 2023 Distributive Corp.
9
9
*/
10
+ 'use strict' ;
10
11
11
12
const { EventTarget, Event } = require ( 'event-target' ) ;
12
13
const { DOMException } = require ( 'dom-exception' ) ;
13
14
const { URL , URLSearchParams } = require ( 'url' ) ;
14
15
const { 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
+ }
15
43
16
44
// exposed
17
45
/**
@@ -29,6 +57,7 @@ class ProgressEvent extends Event
29
57
this . lengthComputable = eventInitDict . lengthComputable ?? false ;
30
58
this . loaded = eventInitDict . loaded ?? 0 ;
31
59
this . total = eventInitDict . total ?? 0 ;
60
+ this . debugTag = 'xhr:' ;
32
61
}
33
62
}
34
63
@@ -112,6 +141,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
112
141
*/
113
142
open ( method , url , async = true , username = null , password = null )
114
143
{
144
+ debug ( 'xhr:open' ) ( 'open start, method=' + method ) ;
115
145
// Normalize the method.
116
146
// @ts -expect-error
117
147
method = method . toString ( ) . toUpperCase ( ) ;
@@ -125,7 +155,8 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
125
155
parsedURL . username = username ;
126
156
if ( password )
127
157
parsedURL . password = password ;
128
-
158
+ debug ( 'xhr:open' ) ( 'url is ' + parsedURL . href ) ;
159
+
129
160
// step 11
130
161
this . #sendFlag = false ;
131
162
this . #uploadListenerFlag = false ;
@@ -144,6 +175,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
144
175
this . #state = XMLHttpRequest . OPENED ;
145
176
this . dispatchEvent ( new Event ( 'readystatechange' ) ) ;
146
177
}
178
+ debug ( 'xhr:open' ) ( 'finished open, state is ' + this . #state) ;
147
179
}
148
180
149
181
/**
@@ -153,6 +185,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
153
185
*/
154
186
setRequestHeader ( name , value )
155
187
{
188
+ debug ( 'xhr:headers' ) ( `set header ${ name } =${ value } ` ) ;
156
189
if ( this . #state !== XMLHttpRequest . OPENED )
157
190
throw new DOMException ( 'setRequestHeader can only be called when state is OPEN' , 'InvalidStateError' ) ;
158
191
if ( this . #sendFlag)
@@ -218,6 +251,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
218
251
*/
219
252
send ( body = null )
220
253
{
254
+ debug ( 'xhr:send' ) ( `sending; body length=${ body ?. length } ` ) ;
221
255
if ( this . #state !== XMLHttpRequest . OPENED ) // step 1
222
256
throw new DOMException ( 'connection must be opened before send() is called' , 'InvalidStateError' ) ;
223
257
if ( this . #sendFlag) // step 2
@@ -248,10 +282,9 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
248
282
249
283
const originalAuthorContentType = this . #requestHeaders[ 'content-type' ] ;
250
284
if ( ! originalAuthorContentType && extractedContentType )
251
- {
252
285
this . #requestHeaders[ 'content-type' ] = extractedContentType ;
253
- }
254
286
}
287
+ debug ( 'xhr:send' ) ( `content-type=${ this . #requestHeaders[ 'content-type' ] } ` ) ;
255
288
256
289
// step 5
257
290
if ( this . #uploadObject. _hasAnyListeners ( ) )
@@ -276,6 +309,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
276
309
*/
277
310
#sendAsync( )
278
311
{
312
+ debug ( 'xhr:send' ) ( 'sending in async mode' ) ;
279
313
this . dispatchEvent ( new ProgressEvent ( 'loadstart' , { loaded :0 , total :0 } ) ) ; // step 11.1
280
314
281
315
let requestBodyTransmitted = 0 ; // step 11.2
@@ -308,6 +342,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
308
342
let responseLength = 0 ;
309
343
const processResponse = ( response ) =>
310
344
{
345
+ debug ( 'xhr:response' ) ( `response headers ----\n${ response . getAllResponseHeaders ( ) } ` ) ;
311
346
this . #response = response ; // step 11.9.1
312
347
this . #state = XMLHttpRequest . HEADERS_RECEIVED ; // step 11.9.4
313
348
this . dispatchEvent ( new Event ( 'readystatechange' ) ) ; // step 11.9.5
@@ -318,6 +353,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
318
353
319
354
const processBodyChunk = ( /** @type {Uint8Array } */ bytes ) =>
320
355
{
356
+ debug ( 'xhr:response' ) ( `recv chunk, ${ bytes . length } bytes (${ trunc ( bytes , 100 ) } )` ) ;
321
357
this . #receivedBytes. push ( bytes ) ;
322
358
if ( this . #state === XMLHttpRequest . HEADERS_RECEIVED )
323
359
this . #state = XMLHttpRequest . LOADING ;
@@ -330,16 +366,22 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
330
366
*/
331
367
const processEndOfBody = ( ) =>
332
368
{
369
+ debug ( 'xhr:response' ) ( `end of body, received ${ this . #receivedLength} bytes` ) ;
333
370
const transmitted = this . #receivedLength; // step 3
334
371
const length = responseLength || 0 ; // step 4
372
+
335
373
this . dispatchEvent ( new ProgressEvent ( 'progress' , { loaded :transmitted , total :length } ) ) ; // step 6
336
374
this . #state = XMLHttpRequest . DONE ; // step 7
337
375
this . #sendFlag = false ; // step 8
376
+
338
377
this . dispatchEvent ( new Event ( 'readystatechange' ) ) ; // step 9
339
378
for ( const eventType of [ 'load' , 'loadend' ] ) // step 10, step 11
340
379
this . dispatchEvent ( new ProgressEvent ( eventType , { loaded :transmitted , total :length } ) ) ;
341
380
} ;
342
381
382
+ debug ( 'xhr:send' ) ( `${ this . #requestMethod} ${ this . #requestURL. href } ` ) ;
383
+ debug ( 'xhr:headers' ) ( 'headers=' + Object . entries ( this . #requestHeaders) ) ;
384
+
343
385
// send() step 6
344
386
request (
345
387
this . #requestMethod,
@@ -362,8 +404,8 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
362
404
*/
363
405
#sendSync( )
364
406
{
407
+ /* Synchronous XHR deprecated. /wg march 2024 */
365
408
throw new DOMException ( 'synchronous XHR is not supported' , 'NotSupportedError' ) ;
366
- // TODO: handle synchronous request
367
409
}
368
410
369
411
/**
@@ -376,7 +418,6 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
376
418
return ;
377
419
if ( this . #timedOutFlag) // step 2
378
420
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`
380
421
if ( this . #response === null /* network error */ ) // step 4
381
422
return this . #reportRequestError( 'error' , new DOMException ( e . toString ( ) , 'NetworkError' ) ) ;
382
423
else // unknown errors
@@ -652,6 +693,10 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
652
693
}
653
694
}
654
695
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
+ */
655
700
if ( ! globalThis . XMLHttpRequestEventTarget )
656
701
globalThis . XMLHttpRequestEventTarget = XMLHttpRequestEventTarget ;
657
702
if ( ! globalThis . XMLHttpRequestUpload )
0 commit comments