Home Reference Source

src/utils/xhr-loader.js

  1. /**
  2. * XHR based logger
  3. */
  4.  
  5. import { logger } from '../utils/logger';
  6.  
  7. class XhrLoader {
  8. constructor (config) {
  9. if (config && config.xhrSetup) {
  10. this.xhrSetup = config.xhrSetup;
  11. }
  12. }
  13.  
  14. destroy () {
  15. this.abort();
  16. this.loader = null;
  17. }
  18.  
  19. abort () {
  20. let loader = this.loader;
  21. if (loader && loader.readyState !== 4) {
  22. this.stats.aborted = true;
  23. loader.abort();
  24. }
  25.  
  26. window.clearTimeout(this.requestTimeout);
  27. this.requestTimeout = null;
  28. window.clearTimeout(this.retryTimeout);
  29. this.retryTimeout = null;
  30. }
  31.  
  32. load (context, config, callbacks) {
  33. this.context = context;
  34. this.config = config;
  35. this.callbacks = callbacks;
  36. this.stats = { trequest: window.performance.now(), retry: 0 };
  37. this.retryDelay = config.retryDelay;
  38. this.loadInternal();
  39. }
  40.  
  41. loadInternal () {
  42. let xhr, context = this.context;
  43. xhr = this.loader = new window.XMLHttpRequest();
  44.  
  45. let stats = this.stats;
  46. stats.tfirst = 0;
  47. stats.loaded = 0;
  48. const xhrSetup = this.xhrSetup;
  49.  
  50. try {
  51. if (xhrSetup) {
  52. try {
  53. xhrSetup(xhr, context.url);
  54. } catch (e) {
  55. // fix xhrSetup: (xhr, url) => {xhr.setRequestHeader("Content-Language", "test");}
  56. // not working, as xhr.setRequestHeader expects xhr.readyState === OPEN
  57. xhr.open('GET', context.url, true);
  58. xhrSetup(xhr, context.url);
  59. }
  60. }
  61. if (!xhr.readyState) {
  62. xhr.open('GET', context.url, true);
  63. }
  64. } catch (e) {
  65. // IE11 throws an exception on xhr.open if attempting to access an HTTP resource over HTTPS
  66. this.callbacks.onError({ code: xhr.status, text: e.message }, context, xhr);
  67. return;
  68. }
  69.  
  70. if (context.rangeEnd) {
  71. xhr.setRequestHeader('Range', 'bytes=' + context.rangeStart + '-' + (context.rangeEnd - 1));
  72. }
  73.  
  74. xhr.onreadystatechange = this.readystatechange.bind(this);
  75. xhr.onprogress = this.loadprogress.bind(this);
  76. xhr.responseType = context.responseType;
  77.  
  78. // setup timeout before we perform request
  79. this.requestTimeout = window.setTimeout(this.loadtimeout.bind(this), this.config.timeout);
  80. xhr.send();
  81. }
  82.  
  83. readystatechange (event) {
  84. let xhr = event.currentTarget,
  85. readyState = xhr.readyState,
  86. stats = this.stats,
  87. context = this.context,
  88. config = this.config;
  89.  
  90. // don't proceed if xhr has been aborted
  91. if (stats.aborted) {
  92. return;
  93. }
  94.  
  95. // >= HEADERS_RECEIVED
  96. if (readyState >= 2) {
  97. // clear xhr timeout and rearm it if readyState less than 4
  98. window.clearTimeout(this.requestTimeout);
  99. if (stats.tfirst === 0) {
  100. stats.tfirst = Math.max(window.performance.now(), stats.trequest);
  101. }
  102.  
  103. if (readyState === 4) {
  104. let status = xhr.status;
  105. // http status between 200 to 299 are all successful
  106. if (status >= 200 && status < 300) {
  107. stats.tload = Math.max(stats.tfirst, window.performance.now());
  108. let data, len;
  109. if (context.responseType === 'arraybuffer') {
  110. data = xhr.response;
  111. len = data.byteLength;
  112. } else {
  113. data = xhr.responseText;
  114. len = data.length;
  115. }
  116. stats.loaded = stats.total = len;
  117. let response = { url: xhr.responseURL, data: data };
  118. this.callbacks.onSuccess(response, stats, context, xhr);
  119. } else {
  120. // if max nb of retries reached or if http status between 400 and 499 (such error cannot be recovered, retrying is useless), return error
  121. if (stats.retry >= config.maxRetry || (status >= 400 && status < 499)) {
  122. logger.error(`${status} while loading ${context.url}`);
  123. this.callbacks.onError({ code: status, text: xhr.statusText }, context, xhr);
  124. } else {
  125. // retry
  126. logger.warn(`${status} while loading ${context.url}, retrying in ${this.retryDelay}...`);
  127. // aborts and resets internal state
  128. this.destroy();
  129. // schedule retry
  130. this.retryTimeout = window.setTimeout(this.loadInternal.bind(this), this.retryDelay);
  131. // set exponential backoff
  132. this.retryDelay = Math.min(2 * this.retryDelay, config.maxRetryDelay);
  133. stats.retry++;
  134. }
  135. }
  136. } else {
  137. // readyState >= 2 AND readyState !==4 (readyState = HEADERS_RECEIVED || LOADING) rearm timeout as xhr not finished yet
  138. this.requestTimeout = window.setTimeout(this.loadtimeout.bind(this), config.timeout);
  139. }
  140. }
  141. }
  142.  
  143. loadtimeout () {
  144. logger.warn(`timeout while loading ${this.context.url}`);
  145. this.callbacks.onTimeout(this.stats, this.context, null);
  146. }
  147.  
  148. loadprogress (event) {
  149. let xhr = event.currentTarget,
  150. stats = this.stats;
  151.  
  152. stats.loaded = event.loaded;
  153. if (event.lengthComputable) {
  154. stats.total = event.total;
  155. }
  156.  
  157. let onProgress = this.callbacks.onProgress;
  158. if (onProgress) {
  159. // third arg is to provide on progress data
  160. onProgress(stats, this.context, null, xhr);
  161. }
  162. }
  163. }
  164.  
  165. export default XhrLoader;