Repositorio del curso CCOM4030 el semestre B91 del proyecto Artesanías con el Instituto de Cultura

AjaxObservable.ts 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. import { root } from '../../util/root';
  2. import { Observable } from '../../Observable';
  3. import { Subscriber } from '../../Subscriber';
  4. import { TeardownLogic } from '../../types';
  5. import { map } from '../../operators/map';
  6. export interface AjaxRequest {
  7. url?: string;
  8. body?: any;
  9. user?: string;
  10. async?: boolean;
  11. method?: string;
  12. headers?: Object;
  13. timeout?: number;
  14. password?: string;
  15. hasContent?: boolean;
  16. crossDomain?: boolean;
  17. withCredentials?: boolean;
  18. createXHR?: () => XMLHttpRequest;
  19. progressSubscriber?: Subscriber<any>;
  20. responseType?: string;
  21. }
  22. function getCORSRequest(): XMLHttpRequest {
  23. if (root.XMLHttpRequest) {
  24. return new root.XMLHttpRequest();
  25. } else if (!!root.XDomainRequest) {
  26. return new root.XDomainRequest();
  27. } else {
  28. throw new Error('CORS is not supported by your browser');
  29. }
  30. }
  31. function getXMLHttpRequest(): XMLHttpRequest {
  32. if (root.XMLHttpRequest) {
  33. return new root.XMLHttpRequest();
  34. } else {
  35. let progId: string;
  36. try {
  37. const progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'];
  38. for (let i = 0; i < 3; i++) {
  39. try {
  40. progId = progIds[i];
  41. if (new root.ActiveXObject(progId)) {
  42. break;
  43. }
  44. } catch (e) {
  45. //suppress exceptions
  46. }
  47. }
  48. return new root.ActiveXObject(progId);
  49. } catch (e) {
  50. throw new Error('XMLHttpRequest is not supported by your browser');
  51. }
  52. }
  53. }
  54. export interface AjaxCreationMethod {
  55. (urlOrRequest: string | AjaxRequest): Observable<AjaxResponse>;
  56. get(url: string, headers?: Object): Observable<AjaxResponse>;
  57. post(url: string, body?: any, headers?: Object): Observable<AjaxResponse>;
  58. put(url: string, body?: any, headers?: Object): Observable<AjaxResponse>;
  59. patch(url: string, body?: any, headers?: Object): Observable<AjaxResponse>;
  60. delete(url: string, headers?: Object): Observable<AjaxResponse>;
  61. getJSON<T>(url: string, headers?: Object): Observable<T>;
  62. }
  63. export function ajaxGet(url: string, headers: Object = null) {
  64. return new AjaxObservable<AjaxResponse>({ method: 'GET', url, headers });
  65. }
  66. export function ajaxPost(url: string, body?: any, headers?: Object): Observable<AjaxResponse> {
  67. return new AjaxObservable<AjaxResponse>({ method: 'POST', url, body, headers });
  68. }
  69. export function ajaxDelete(url: string, headers?: Object): Observable<AjaxResponse> {
  70. return new AjaxObservable<AjaxResponse>({ method: 'DELETE', url, headers });
  71. }
  72. export function ajaxPut(url: string, body?: any, headers?: Object): Observable<AjaxResponse> {
  73. return new AjaxObservable<AjaxResponse>({ method: 'PUT', url, body, headers });
  74. }
  75. export function ajaxPatch(url: string, body?: any, headers?: Object): Observable<AjaxResponse> {
  76. return new AjaxObservable<AjaxResponse>({ method: 'PATCH', url, body, headers });
  77. }
  78. const mapResponse = map((x: AjaxResponse, index: number) => x.response);
  79. export function ajaxGetJSON<T>(url: string, headers?: Object): Observable<T> {
  80. return mapResponse(
  81. new AjaxObservable<AjaxResponse>({
  82. method: 'GET',
  83. url,
  84. responseType: 'json',
  85. headers
  86. })
  87. );
  88. }
  89. /**
  90. * We need this JSDoc comment for affecting ESDoc.
  91. * @extends {Ignored}
  92. * @hide true
  93. */
  94. export class AjaxObservable<T> extends Observable<T> {
  95. /**
  96. * Creates an observable for an Ajax request with either a request object with
  97. * url, headers, etc or a string for a URL.
  98. *
  99. * ## Example
  100. * ```ts
  101. * import { ajax } from 'rxjs/ajax';
  102. *
  103. * const source1 = ajax('/products');
  104. * const source2 = ajax({ url: 'products', method: 'GET' });
  105. * ```
  106. *
  107. * @param {string|Object} request Can be one of the following:
  108. * A string of the URL to make the Ajax call.
  109. * An object with the following properties
  110. * - url: URL of the request
  111. * - body: The body of the request
  112. * - method: Method of the request, such as GET, POST, PUT, PATCH, DELETE
  113. * - async: Whether the request is async
  114. * - headers: Optional headers
  115. * - crossDomain: true if a cross domain request, else false
  116. * - createXHR: a function to override if you need to use an alternate
  117. * XMLHttpRequest implementation.
  118. * - resultSelector: a function to use to alter the output value type of
  119. * the Observable. Gets {@link AjaxResponse} as an argument.
  120. * @return {Observable} An observable sequence containing the XMLHttpRequest.
  121. * @static true
  122. * @name ajax
  123. * @owner Observable
  124. * @nocollapse
  125. */
  126. static create: AjaxCreationMethod = (() => {
  127. const create: any = (urlOrRequest: string | AjaxRequest) => {
  128. return new AjaxObservable(urlOrRequest);
  129. };
  130. create.get = ajaxGet;
  131. create.post = ajaxPost;
  132. create.delete = ajaxDelete;
  133. create.put = ajaxPut;
  134. create.patch = ajaxPatch;
  135. create.getJSON = ajaxGetJSON;
  136. return <AjaxCreationMethod>create;
  137. })();
  138. private request: AjaxRequest;
  139. constructor(urlOrRequest: string | AjaxRequest) {
  140. super();
  141. const request: AjaxRequest = {
  142. async: true,
  143. createXHR: function(this: AjaxRequest) {
  144. return this.crossDomain ? getCORSRequest() : getXMLHttpRequest();
  145. },
  146. crossDomain: true,
  147. withCredentials: false,
  148. headers: {},
  149. method: 'GET',
  150. responseType: 'json',
  151. timeout: 0
  152. };
  153. if (typeof urlOrRequest === 'string') {
  154. request.url = urlOrRequest;
  155. } else {
  156. for (const prop in urlOrRequest) {
  157. if (urlOrRequest.hasOwnProperty(prop)) {
  158. request[prop] = urlOrRequest[prop];
  159. }
  160. }
  161. }
  162. this.request = request;
  163. }
  164. /** @deprecated This is an internal implementation detail, do not use. */
  165. _subscribe(subscriber: Subscriber<T>): TeardownLogic {
  166. return new AjaxSubscriber(subscriber, this.request);
  167. }
  168. }
  169. /**
  170. * We need this JSDoc comment for affecting ESDoc.
  171. * @ignore
  172. * @extends {Ignored}
  173. */
  174. export class AjaxSubscriber<T> extends Subscriber<Event> {
  175. private xhr: XMLHttpRequest;
  176. private done: boolean = false;
  177. constructor(destination: Subscriber<T>, public request: AjaxRequest) {
  178. super(destination);
  179. const headers = request.headers = request.headers || {};
  180. // force CORS if requested
  181. if (!request.crossDomain && !this.getHeader(headers, 'X-Requested-With')) {
  182. headers['X-Requested-With'] = 'XMLHttpRequest';
  183. }
  184. // ensure content type is set
  185. let contentTypeHeader = this.getHeader(headers, 'Content-Type');
  186. if (!contentTypeHeader && !(root.FormData && request.body instanceof root.FormData) && typeof request.body !== 'undefined') {
  187. headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
  188. }
  189. // properly serialize body
  190. request.body = this.serializeBody(request.body, this.getHeader(request.headers, 'Content-Type'));
  191. this.send();
  192. }
  193. next(e: Event): void {
  194. this.done = true;
  195. const { xhr, request, destination } = this;
  196. let result;
  197. try {
  198. result = new AjaxResponse(e, xhr, request);
  199. } catch (err) {
  200. return destination.error(err);
  201. }
  202. destination.next(result);
  203. }
  204. private send(): void {
  205. const {
  206. request,
  207. request: { user, method, url, async, password, headers, body }
  208. } = this;
  209. try {
  210. const xhr = this.xhr = request.createXHR();
  211. // set up the events before open XHR
  212. // https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest
  213. // You need to add the event listeners before calling open() on the request.
  214. // Otherwise the progress events will not fire.
  215. this.setupEvents(xhr, request);
  216. // open XHR
  217. if (user) {
  218. xhr.open(method, url, async, user, password);
  219. } else {
  220. xhr.open(method, url, async);
  221. }
  222. // timeout, responseType and withCredentials can be set once the XHR is open
  223. if (async) {
  224. xhr.timeout = request.timeout;
  225. xhr.responseType = request.responseType as any;
  226. }
  227. if ('withCredentials' in xhr) {
  228. xhr.withCredentials = !!request.withCredentials;
  229. }
  230. // set headers
  231. this.setHeaders(xhr, headers);
  232. // finally send the request
  233. if (body) {
  234. xhr.send(body);
  235. } else {
  236. xhr.send();
  237. }
  238. } catch (err) {
  239. this.error(err);
  240. }
  241. }
  242. private serializeBody(body: any, contentType?: string) {
  243. if (!body || typeof body === 'string') {
  244. return body;
  245. } else if (root.FormData && body instanceof root.FormData) {
  246. return body;
  247. }
  248. if (contentType) {
  249. const splitIndex = contentType.indexOf(';');
  250. if (splitIndex !== -1) {
  251. contentType = contentType.substring(0, splitIndex);
  252. }
  253. }
  254. switch (contentType) {
  255. case 'application/x-www-form-urlencoded':
  256. return Object.keys(body).map(key => `${encodeURIComponent(key)}=${encodeURIComponent(body[key])}`).join('&');
  257. case 'application/json':
  258. return JSON.stringify(body);
  259. default:
  260. return body;
  261. }
  262. }
  263. private setHeaders(xhr: XMLHttpRequest, headers: Object) {
  264. for (let key in headers) {
  265. if (headers.hasOwnProperty(key)) {
  266. xhr.setRequestHeader(key, headers[key]);
  267. }
  268. }
  269. }
  270. private getHeader(headers: {}, headerName: string): any {
  271. for (let key in headers) {
  272. if (key.toLowerCase() === headerName.toLowerCase()) {
  273. return headers[key];
  274. }
  275. }
  276. return undefined;
  277. }
  278. private setupEvents(xhr: XMLHttpRequest, request: AjaxRequest) {
  279. const progressSubscriber = request.progressSubscriber;
  280. function xhrTimeout(this: XMLHttpRequest, e: ProgressEvent): void {
  281. const {subscriber, progressSubscriber, request } = (<any>xhrTimeout);
  282. if (progressSubscriber) {
  283. progressSubscriber.error(e);
  284. }
  285. let error;
  286. try {
  287. error = new AjaxTimeoutError(this, request); // TODO: Make betterer.
  288. } catch (err) {
  289. error = err;
  290. }
  291. subscriber.error(error);
  292. }
  293. xhr.ontimeout = xhrTimeout;
  294. (<any>xhrTimeout).request = request;
  295. (<any>xhrTimeout).subscriber = this;
  296. (<any>xhrTimeout).progressSubscriber = progressSubscriber;
  297. if (xhr.upload && 'withCredentials' in xhr) {
  298. if (progressSubscriber) {
  299. let xhrProgress: (e: ProgressEvent) => void;
  300. xhrProgress = function(e: ProgressEvent) {
  301. const { progressSubscriber } = (<any>xhrProgress);
  302. progressSubscriber.next(e);
  303. };
  304. if (root.XDomainRequest) {
  305. xhr.onprogress = xhrProgress;
  306. } else {
  307. xhr.upload.onprogress = xhrProgress;
  308. }
  309. (<any>xhrProgress).progressSubscriber = progressSubscriber;
  310. }
  311. let xhrError: (e: any) => void;
  312. xhrError = function(this: XMLHttpRequest, e: ErrorEvent) {
  313. const { progressSubscriber, subscriber, request } = (<any>xhrError);
  314. if (progressSubscriber) {
  315. progressSubscriber.error(e);
  316. }
  317. let error;
  318. try {
  319. error = new AjaxError('ajax error', this, request);
  320. } catch (err) {
  321. error = err;
  322. }
  323. subscriber.error(error);
  324. };
  325. xhr.onerror = xhrError;
  326. (<any>xhrError).request = request;
  327. (<any>xhrError).subscriber = this;
  328. (<any>xhrError).progressSubscriber = progressSubscriber;
  329. }
  330. function xhrReadyStateChange(this: XMLHttpRequest, e: Event) {
  331. return;
  332. }
  333. xhr.onreadystatechange = xhrReadyStateChange;
  334. (<any>xhrReadyStateChange).subscriber = this;
  335. (<any>xhrReadyStateChange).progressSubscriber = progressSubscriber;
  336. (<any>xhrReadyStateChange).request = request;
  337. function xhrLoad(this: XMLHttpRequest, e: Event) {
  338. const { subscriber, progressSubscriber, request } = (<any>xhrLoad);
  339. if (this.readyState === 4) {
  340. // normalize IE9 bug (http://bugs.jquery.com/ticket/1450)
  341. let status: number = this.status === 1223 ? 204 : this.status;
  342. let response: any = (this.responseType === 'text' ? (
  343. this.response || this.responseText) : this.response);
  344. // fix status code when it is 0 (0 status is undocumented).
  345. // Occurs when accessing file resources or on Android 4.1 stock browser
  346. // while retrieving files from application cache.
  347. if (status === 0) {
  348. status = response ? 200 : 0;
  349. }
  350. // 4xx and 5xx should error (https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)
  351. if (status < 400) {
  352. if (progressSubscriber) {
  353. progressSubscriber.complete();
  354. }
  355. subscriber.next(e);
  356. subscriber.complete();
  357. } else {
  358. if (progressSubscriber) {
  359. progressSubscriber.error(e);
  360. }
  361. let error;
  362. try {
  363. error = new AjaxError('ajax error ' + status, this, request);
  364. } catch (err) {
  365. error = err;
  366. }
  367. subscriber.error(error);
  368. }
  369. }
  370. }
  371. xhr.onload = xhrLoad;
  372. (<any>xhrLoad).subscriber = this;
  373. (<any>xhrLoad).progressSubscriber = progressSubscriber;
  374. (<any>xhrLoad).request = request;
  375. }
  376. unsubscribe() {
  377. const { done, xhr } = this;
  378. if (!done && xhr && xhr.readyState !== 4 && typeof xhr.abort === 'function') {
  379. xhr.abort();
  380. }
  381. super.unsubscribe();
  382. }
  383. }
  384. /**
  385. * A normalized AJAX response.
  386. *
  387. * @see {@link ajax}
  388. *
  389. * @class AjaxResponse
  390. */
  391. export class AjaxResponse {
  392. /** @type {number} The HTTP status code */
  393. status: number;
  394. /** @type {string|ArrayBuffer|Document|object|any} The response data */
  395. response: any;
  396. /** @type {string} The raw responseText */
  397. responseText: string;
  398. /** @type {string} The responseType (e.g. 'json', 'arraybuffer', or 'xml') */
  399. responseType: string;
  400. constructor(public originalEvent: Event, public xhr: XMLHttpRequest, public request: AjaxRequest) {
  401. this.status = xhr.status;
  402. this.responseType = xhr.responseType || request.responseType;
  403. this.response = parseXhrResponse(this.responseType, xhr);
  404. }
  405. }
  406. export type AjaxErrorNames = 'AjaxError' | 'AjaxTimeoutError';
  407. /**
  408. * A normalized AJAX error.
  409. *
  410. * @see {@link ajax}
  411. *
  412. * @class AjaxError
  413. */
  414. export interface AjaxError extends Error {
  415. /** @type {XMLHttpRequest} The XHR instance associated with the error */
  416. xhr: XMLHttpRequest;
  417. /** @type {AjaxRequest} The AjaxRequest associated with the error */
  418. request: AjaxRequest;
  419. /** @type {number} The HTTP status code */
  420. status: number;
  421. /** @type {string} The responseType (e.g. 'json', 'arraybuffer', or 'xml') */
  422. responseType: string;
  423. /** @type {string|ArrayBuffer|Document|object|any} The response data */
  424. response: any;
  425. }
  426. export interface AjaxErrorCtor {
  427. new(message: string, xhr: XMLHttpRequest, request: AjaxRequest): AjaxError;
  428. }
  429. const AjaxErrorImpl = (() => {
  430. function AjaxErrorImpl(this: any, message: string, xhr: XMLHttpRequest, request: AjaxRequest): AjaxError {
  431. Error.call(this);
  432. this.message = message;
  433. this.name = 'AjaxError';
  434. this.xhr = xhr;
  435. this.request = request;
  436. this.status = xhr.status;
  437. this.responseType = xhr.responseType || request.responseType;
  438. this.response = parseXhrResponse(this.responseType, xhr);
  439. return this;
  440. }
  441. AjaxErrorImpl.prototype = Object.create(Error.prototype);
  442. return AjaxErrorImpl;
  443. })();
  444. export const AjaxError: AjaxErrorCtor = AjaxErrorImpl as any;
  445. function parseJson(xhr: XMLHttpRequest) {
  446. // HACK(benlesh): TypeScript shennanigans
  447. // tslint:disable-next-line:no-any XMLHttpRequest is defined to always have 'response' inferring xhr as never for the else clause.
  448. if ('response' in (xhr as any)) {
  449. //IE does not support json as responseType, parse it internally
  450. return xhr.responseType ? xhr.response : JSON.parse(xhr.response || xhr.responseText || 'null');
  451. } else {
  452. return JSON.parse((xhr as any).responseText || 'null');
  453. }
  454. }
  455. function parseXhrResponse(responseType: string, xhr: XMLHttpRequest) {
  456. switch (responseType) {
  457. case 'json':
  458. return parseJson(xhr);
  459. case 'xml':
  460. return xhr.responseXML;
  461. case 'text':
  462. default:
  463. // HACK(benlesh): TypeScript shennanigans
  464. // tslint:disable-next-line:no-any XMLHttpRequest is defined to always have 'response' inferring xhr as never for the else sub-expression.
  465. return ('response' in (xhr as any)) ? xhr.response : xhr.responseText;
  466. }
  467. }
  468. export interface AjaxTimeoutError extends AjaxError {
  469. }
  470. export interface AjaxTimeoutErrorCtor {
  471. new(xhr: XMLHttpRequest, request: AjaxRequest): AjaxTimeoutError;
  472. }
  473. function AjaxTimeoutErrorImpl(this: any, xhr: XMLHttpRequest, request: AjaxRequest) {
  474. AjaxError.call(this, 'ajax timeout', xhr, request);
  475. this.name = 'AjaxTimeoutError';
  476. return this;
  477. }
  478. /**
  479. * @see {@link ajax}
  480. *
  481. * @class AjaxTimeoutError
  482. */
  483. export const AjaxTimeoutError: AjaxTimeoutErrorCtor = AjaxTimeoutErrorImpl as any;