| import { map } from '../operators/map'; |
| import { Observable } from '../Observable'; |
| import { AjaxConfig, AjaxRequest, AjaxDirection, ProgressEventType } from './types'; |
| import { AjaxResponse } from './AjaxResponse'; |
| import { AjaxTimeoutError, AjaxError } from './errors'; |
| |
| export interface AjaxCreationMethod { |
| /** |
| * Creates an observable that will perform an AJAX request using the |
| * [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) in |
| * global scope by default. |
| * |
| * This is the most configurable option, and the basis for all other AJAX calls in the library. |
| * |
| * ## Example |
| * |
| * ```ts |
| * import { ajax } from 'rxjs/ajax'; |
| * import { map, catchError, of } from 'rxjs'; |
| * |
| * const obs$ = ajax({ |
| * method: 'GET', |
| * url: 'https://api.github.com/users?per_page=5', |
| * responseType: 'json' |
| * }).pipe( |
| * map(userResponse => console.log('users: ', userResponse)), |
| * catchError(error => { |
| * console.log('error: ', error); |
| * return of(error); |
| * }) |
| * ); |
| * ``` |
| */ |
| <T>(config: AjaxConfig): Observable<AjaxResponse<T>>; |
| |
| /** |
| * Perform an HTTP GET using the |
| * [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) in |
| * global scope. Defaults to a `responseType` of `"json"`. |
| * |
| * ## Example |
| * |
| * ```ts |
| * import { ajax } from 'rxjs/ajax'; |
| * import { map, catchError, of } from 'rxjs'; |
| * |
| * const obs$ = ajax('https://api.github.com/users?per_page=5').pipe( |
| * map(userResponse => console.log('users: ', userResponse)), |
| * catchError(error => { |
| * console.log('error: ', error); |
| * return of(error); |
| * }) |
| * ); |
| * ``` |
| */ |
| <T>(url: string): Observable<AjaxResponse<T>>; |
| |
| /** |
| * Performs an HTTP GET using the |
| * [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) in |
| * global scope by default, and a `responseType` of `"json"`. |
| * |
| * @param url The URL to get the resource from |
| * @param headers Optional headers. Case-Insensitive. |
| */ |
| get<T>(url: string, headers?: Record<string, string>): Observable<AjaxResponse<T>>; |
| |
| /** |
| * Performs an HTTP POST using the |
| * [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) in |
| * global scope by default, and a `responseType` of `"json"`. |
| * |
| * Before sending the value passed to the `body` argument, it is automatically serialized |
| * based on the specified `responseType`. By default, a JavaScript object will be serialized |
| * to JSON. A `responseType` of `application/x-www-form-urlencoded` will flatten any provided |
| * dictionary object to a url-encoded string. |
| * |
| * @param url The URL to get the resource from |
| * @param body The content to send. The body is automatically serialized. |
| * @param headers Optional headers. Case-Insensitive. |
| */ |
| post<T>(url: string, body?: any, headers?: Record<string, string>): Observable<AjaxResponse<T>>; |
| |
| /** |
| * Performs an HTTP PUT using the |
| * [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) in |
| * global scope by default, and a `responseType` of `"json"`. |
| * |
| * Before sending the value passed to the `body` argument, it is automatically serialized |
| * based on the specified `responseType`. By default, a JavaScript object will be serialized |
| * to JSON. A `responseType` of `application/x-www-form-urlencoded` will flatten any provided |
| * dictionary object to a url-encoded string. |
| * |
| * @param url The URL to get the resource from |
| * @param body The content to send. The body is automatically serialized. |
| * @param headers Optional headers. Case-Insensitive. |
| */ |
| put<T>(url: string, body?: any, headers?: Record<string, string>): Observable<AjaxResponse<T>>; |
| |
| /** |
| * Performs an HTTP PATCH using the |
| * [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) in |
| * global scope by default, and a `responseType` of `"json"`. |
| * |
| * Before sending the value passed to the `body` argument, it is automatically serialized |
| * based on the specified `responseType`. By default, a JavaScript object will be serialized |
| * to JSON. A `responseType` of `application/x-www-form-urlencoded` will flatten any provided |
| * dictionary object to a url-encoded string. |
| * |
| * @param url The URL to get the resource from |
| * @param body The content to send. The body is automatically serialized. |
| * @param headers Optional headers. Case-Insensitive. |
| */ |
| patch<T>(url: string, body?: any, headers?: Record<string, string>): Observable<AjaxResponse<T>>; |
| |
| /** |
| * Performs an HTTP DELETE using the |
| * [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) in |
| * global scope by default, and a `responseType` of `"json"`. |
| * |
| * @param url The URL to get the resource from |
| * @param headers Optional headers. Case-Insensitive. |
| */ |
| delete<T>(url: string, headers?: Record<string, string>): Observable<AjaxResponse<T>>; |
| |
| /** |
| * Performs an HTTP GET using the |
| * [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) in |
| * global scope by default, and returns the hydrated JavaScript object from the |
| * response. |
| * |
| * @param url The URL to get the resource from |
| * @param headers Optional headers. Case-Insensitive. |
| */ |
| getJSON<T>(url: string, headers?: Record<string, string>): Observable<T>; |
| } |
| |
| function ajaxGet<T>(url: string, headers?: Record<string, string>): Observable<AjaxResponse<T>> { |
| return ajax({ method: 'GET', url, headers }); |
| } |
| |
| function ajaxPost<T>(url: string, body?: any, headers?: Record<string, string>): Observable<AjaxResponse<T>> { |
| return ajax({ method: 'POST', url, body, headers }); |
| } |
| |
| function ajaxDelete<T>(url: string, headers?: Record<string, string>): Observable<AjaxResponse<T>> { |
| return ajax({ method: 'DELETE', url, headers }); |
| } |
| |
| function ajaxPut<T>(url: string, body?: any, headers?: Record<string, string>): Observable<AjaxResponse<T>> { |
| return ajax({ method: 'PUT', url, body, headers }); |
| } |
| |
| function ajaxPatch<T>(url: string, body?: any, headers?: Record<string, string>): Observable<AjaxResponse<T>> { |
| return ajax({ method: 'PATCH', url, body, headers }); |
| } |
| |
| const mapResponse = map((x: AjaxResponse<any>) => x.response); |
| |
| function ajaxGetJSON<T>(url: string, headers?: Record<string, string>): Observable<T> { |
| return mapResponse( |
| ajax<T>({ |
| method: 'GET', |
| url, |
| headers, |
| }) |
| ); |
| } |
| |
| /** |
| * There is an ajax operator on the Rx object. |
| * |
| * It creates an observable for an Ajax request with either a request object with |
| * url, headers, etc or a string for a URL. |
| * |
| * ## Examples |
| * |
| * Using `ajax()` to fetch the response object that is being returned from API |
| * |
| * ```ts |
| * import { ajax } from 'rxjs/ajax'; |
| * import { map, catchError, of } from 'rxjs'; |
| * |
| * const obs$ = ajax('https://api.github.com/users?per_page=5').pipe( |
| * map(userResponse => console.log('users: ', userResponse)), |
| * catchError(error => { |
| * console.log('error: ', error); |
| * return of(error); |
| * }) |
| * ); |
| * |
| * obs$.subscribe({ |
| * next: value => console.log(value), |
| * error: err => console.log(err) |
| * }); |
| * ``` |
| * |
| * Using `ajax.getJSON()` to fetch data from API |
| * |
| * ```ts |
| * import { ajax } from 'rxjs/ajax'; |
| * import { map, catchError, of } from 'rxjs'; |
| * |
| * const obs$ = ajax.getJSON('https://api.github.com/users?per_page=5').pipe( |
| * map(userResponse => console.log('users: ', userResponse)), |
| * catchError(error => { |
| * console.log('error: ', error); |
| * return of(error); |
| * }) |
| * ); |
| * |
| * obs$.subscribe({ |
| * next: value => console.log(value), |
| * error: err => console.log(err) |
| * }); |
| * ``` |
| * |
| * Using `ajax()` with object as argument and method POST with a two seconds delay |
| * |
| * ```ts |
| * import { ajax } from 'rxjs/ajax'; |
| * import { map, catchError, of } from 'rxjs'; |
| * |
| * const users = ajax({ |
| * url: 'https://httpbin.org/delay/2', |
| * method: 'POST', |
| * headers: { |
| * 'Content-Type': 'application/json', |
| * 'rxjs-custom-header': 'Rxjs' |
| * }, |
| * body: { |
| * rxjs: 'Hello World!' |
| * } |
| * }).pipe( |
| * map(response => console.log('response: ', response)), |
| * catchError(error => { |
| * console.log('error: ', error); |
| * return of(error); |
| * }) |
| * ); |
| * |
| * users.subscribe({ |
| * next: value => console.log(value), |
| * error: err => console.log(err) |
| * }); |
| * ``` |
| * |
| * Using `ajax()` to fetch. An error object that is being returned from the request |
| * |
| * ```ts |
| * import { ajax } from 'rxjs/ajax'; |
| * import { map, catchError, of } from 'rxjs'; |
| * |
| * const obs$ = ajax('https://api.github.com/404').pipe( |
| * map(userResponse => console.log('users: ', userResponse)), |
| * catchError(error => { |
| * console.log('error: ', error); |
| * return of(error); |
| * }) |
| * ); |
| * |
| * obs$.subscribe({ |
| * next: value => console.log(value), |
| * error: err => console.log(err) |
| * }); |
| * ``` |
| */ |
| export const ajax: AjaxCreationMethod = (() => { |
| const create = <T>(urlOrConfig: string | AjaxConfig) => { |
| const config: AjaxConfig = |
| typeof urlOrConfig === 'string' |
| ? { |
| url: urlOrConfig, |
| } |
| : urlOrConfig; |
| return fromAjax<T>(config); |
| }; |
| |
| create.get = ajaxGet; |
| create.post = ajaxPost; |
| create.delete = ajaxDelete; |
| create.put = ajaxPut; |
| create.patch = ajaxPatch; |
| create.getJSON = ajaxGetJSON; |
| |
| return create; |
| })(); |
| |
| const UPLOAD = 'upload'; |
| const DOWNLOAD = 'download'; |
| const LOADSTART = 'loadstart'; |
| const PROGRESS = 'progress'; |
| const LOAD = 'load'; |
| |
| export function fromAjax<T>(init: AjaxConfig): Observable<AjaxResponse<T>> { |
| return new Observable((destination) => { |
| const config = { |
| // Defaults |
| async: true, |
| crossDomain: false, |
| withCredentials: false, |
| method: 'GET', |
| timeout: 0, |
| responseType: 'json' as XMLHttpRequestResponseType, |
| |
| ...init, |
| }; |
| |
| const { queryParams, body: configuredBody, headers: configuredHeaders } = config; |
| |
| let url = config.url; |
| if (!url) { |
| throw new TypeError('url is required'); |
| } |
| |
| if (queryParams) { |
| let searchParams: URLSearchParams; |
| if (url.includes('?')) { |
| // If the user has passed a URL with a querystring already in it, |
| // we need to combine them. So we're going to split it. There |
| // should only be one `?` in a valid URL. |
| const parts = url.split('?'); |
| if (2 < parts.length) { |
| throw new TypeError('invalid url'); |
| } |
| // Add the passed queryParams to the params already in the url provided. |
| searchParams = new URLSearchParams(parts[1]); |
| // queryParams is converted to any because the runtime is *much* more permissive than |
| // the types are. |
| new URLSearchParams(queryParams as any).forEach((value, key) => searchParams.set(key, value)); |
| // We have to do string concatenation here, because `new URL(url)` does |
| // not like relative URLs like `/this` without a base url, which we can't |
| // specify, nor can we assume `location` will exist, because of node. |
| url = parts[0] + '?' + searchParams; |
| } else { |
| // There is no preexisting querystring, so we can just use URLSearchParams |
| // to convert the passed queryParams into the proper format and encodings. |
| // queryParams is converted to any because the runtime is *much* more permissive than |
| // the types are. |
| searchParams = new URLSearchParams(queryParams as any); |
| url = url + '?' + searchParams; |
| } |
| } |
| |
| // Normalize the headers. We're going to make them all lowercase, since |
| // Headers are case insensitive by design. This makes it easier to verify |
| // that we aren't setting or sending duplicates. |
| const headers: Record<string, any> = {}; |
| if (configuredHeaders) { |
| for (const key in configuredHeaders) { |
| if (configuredHeaders.hasOwnProperty(key)) { |
| headers[key.toLowerCase()] = configuredHeaders[key]; |
| } |
| } |
| } |
| |
| const crossDomain = config.crossDomain; |
| |
| // Set the x-requested-with header. This is a non-standard header that has |
| // come to be a de facto standard for HTTP requests sent by libraries and frameworks |
| // using XHR. However, we DO NOT want to set this if it is a CORS request. This is |
| // because sometimes this header can cause issues with CORS. To be clear, |
| // None of this is necessary, it's only being set because it's "the thing libraries do" |
| // Starting back as far as JQuery, and continuing with other libraries such as Angular 1, |
| // Axios, et al. |
| if (!crossDomain && !('x-requested-with' in headers)) { |
| headers['x-requested-with'] = 'XMLHttpRequest'; |
| } |
| |
| // Allow users to provide their XSRF cookie name and the name of a custom header to use to |
| // send the cookie. |
| const { withCredentials, xsrfCookieName, xsrfHeaderName } = config; |
| if ((withCredentials || !crossDomain) && xsrfCookieName && xsrfHeaderName) { |
| const xsrfCookie = document?.cookie.match(new RegExp(`(^|;\\s*)(${xsrfCookieName})=([^;]*)`))?.pop() ?? ''; |
| if (xsrfCookie) { |
| headers[xsrfHeaderName] = xsrfCookie; |
| } |
| } |
| |
| // Examine the body and determine whether or not to serialize it |
| // and set the content-type in `headers`, if we're able. |
| const body = extractContentTypeAndMaybeSerializeBody(configuredBody, headers); |
| |
| // The final request settings. |
| const _request: Readonly<AjaxRequest> = { |
| ...config, |
| |
| // Set values we ensured above |
| url, |
| headers, |
| body, |
| }; |
| |
| let xhr: XMLHttpRequest; |
| |
| // Create our XHR so we can get started. |
| xhr = init.createXHR ? init.createXHR() : new XMLHttpRequest(); |
| |
| { |
| /////////////////////////////////////////////////// |
| // set up the events before open XHR |
| // https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest |
| // You need to add the event listeners before calling open() on the request. |
| // Otherwise the progress events will not fire. |
| /////////////////////////////////////////////////// |
| |
| const { progressSubscriber, includeDownloadProgress = false, includeUploadProgress = false } = init; |
| |
| /** |
| * Wires up an event handler that will emit an error when fired. Used |
| * for timeout and abort events. |
| * @param type The type of event we're treating as an error |
| * @param errorFactory A function that creates the type of error to emit. |
| */ |
| const addErrorEvent = (type: string, errorFactory: () => any) => { |
| xhr.addEventListener(type, () => { |
| const error = errorFactory(); |
| progressSubscriber?.error?.(error); |
| destination.error(error); |
| }); |
| }; |
| |
| // If the request times out, handle errors appropriately. |
| addErrorEvent('timeout', () => new AjaxTimeoutError(xhr, _request)); |
| |
| // If the request aborts (due to a network disconnection or the like), handle |
| // it as an error. |
| addErrorEvent('abort', () => new AjaxError('aborted', xhr, _request)); |
| |
| /** |
| * Creates a response object to emit to the consumer. |
| * @param direction the direction related to the event. Prefixes the event `type` in the |
| * `AjaxResponse` object with "upload_" for events related to uploading and "download_" |
| * for events related to downloading. |
| * @param event the actual event object. |
| */ |
| const createResponse = (direction: AjaxDirection, event: ProgressEvent) => |
| new AjaxResponse<T>(event, xhr, _request, `${direction}_${event.type as ProgressEventType}` as const); |
| |
| /** |
| * Wires up an event handler that emits a Response object to the consumer, used for |
| * all events that emit responses, loadstart, progress, and load. |
| * Note that download load handling is a bit different below, because it has |
| * more logic it needs to run. |
| * @param target The target, either the XHR itself or the Upload object. |
| * @param type The type of event to wire up |
| * @param direction The "direction", used to prefix the response object that is |
| * emitted to the consumer. (e.g. "upload_" or "download_") |
| */ |
| const addProgressEvent = (target: any, type: string, direction: AjaxDirection) => { |
| target.addEventListener(type, (event: ProgressEvent) => { |
| destination.next(createResponse(direction, event)); |
| }); |
| }; |
| |
| if (includeUploadProgress) { |
| [LOADSTART, PROGRESS, LOAD].forEach((type) => addProgressEvent(xhr.upload, type, UPLOAD)); |
| } |
| |
| if (progressSubscriber) { |
| [LOADSTART, PROGRESS].forEach((type) => xhr.upload.addEventListener(type, (e: any) => progressSubscriber?.next?.(e))); |
| } |
| |
| if (includeDownloadProgress) { |
| [LOADSTART, PROGRESS].forEach((type) => addProgressEvent(xhr, type, DOWNLOAD)); |
| } |
| |
| const emitError = (status?: number) => { |
| const msg = 'ajax error' + (status ? ' ' + status : ''); |
| destination.error(new AjaxError(msg, xhr, _request)); |
| }; |
| |
| xhr.addEventListener('error', (e) => { |
| progressSubscriber?.error?.(e); |
| emitError(); |
| }); |
| |
| xhr.addEventListener(LOAD, (event) => { |
| const { status } = xhr; |
| // 4xx and 5xx should error (https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html) |
| if (status < 400) { |
| progressSubscriber?.complete?.(); |
| |
| let response: AjaxResponse<T>; |
| try { |
| // This can throw in IE, because we end up needing to do a JSON.parse |
| // of the response in some cases to produce object we'd expect from |
| // modern browsers. |
| response = createResponse(DOWNLOAD, event); |
| } catch (err) { |
| destination.error(err); |
| return; |
| } |
| |
| destination.next(response); |
| destination.complete(); |
| } else { |
| progressSubscriber?.error?.(event); |
| emitError(status); |
| } |
| }); |
| } |
| |
| const { user, method, async } = _request; |
| // open XHR |
| if (user) { |
| xhr.open(method, url, async, user, _request.password); |
| } else { |
| xhr.open(method, url, async); |
| } |
| |
| // timeout, responseType and withCredentials can be set once the XHR is open |
| if (async) { |
| xhr.timeout = _request.timeout; |
| xhr.responseType = _request.responseType; |
| } |
| |
| if ('withCredentials' in xhr) { |
| xhr.withCredentials = _request.withCredentials; |
| } |
| |
| // set headers |
| for (const key in headers) { |
| if (headers.hasOwnProperty(key)) { |
| xhr.setRequestHeader(key, headers[key]); |
| } |
| } |
| |
| // finally send the request |
| if (body) { |
| xhr.send(body); |
| } else { |
| xhr.send(); |
| } |
| |
| return () => { |
| if (xhr && xhr.readyState !== 4 /*XHR done*/) { |
| xhr.abort(); |
| } |
| }; |
| }); |
| } |
| |
| /** |
| * Examines the body to determine if we need to serialize it for them or not. |
| * If the body is a type that XHR handles natively, we just allow it through, |
| * otherwise, if the body is something that *we* can serialize for the user, |
| * we will serialize it, and attempt to set the `content-type` header, if it's |
| * not already set. |
| * @param body The body passed in by the user |
| * @param headers The normalized headers |
| */ |
| function extractContentTypeAndMaybeSerializeBody(body: any, headers: Record<string, string>) { |
| if ( |
| !body || |
| typeof body === 'string' || |
| isFormData(body) || |
| isURLSearchParams(body) || |
| isArrayBuffer(body) || |
| isFile(body) || |
| isBlob(body) || |
| isReadableStream(body) |
| ) { |
| // The XHR instance itself can handle serializing these, and set the content-type for us |
| // so we don't need to do that. https://xhr.spec.whatwg.org/#the-send()-method |
| return body; |
| } |
| |
| if (isArrayBufferView(body)) { |
| // This is a typed array (e.g. Float32Array or Uint8Array), or a DataView. |
| // XHR can handle this one too: https://fetch.spec.whatwg.org/#concept-bodyinit-extract |
| return body.buffer; |
| } |
| |
| if (typeof body === 'object') { |
| // If we have made it here, this is an object, probably a POJO, and we'll try |
| // to serialize it for them. If this doesn't work, it will throw, obviously, which |
| // is okay. The workaround for users would be to manually set the body to their own |
| // serialized string (accounting for circular references or whatever), then set |
| // the content-type manually as well. |
| headers['content-type'] = headers['content-type'] ?? 'application/json;charset=utf-8'; |
| return JSON.stringify(body); |
| } |
| |
| // If we've gotten past everything above, this is something we don't quite know how to |
| // handle. Throw an error. This will be caught and emitted from the observable. |
| throw new TypeError('Unknown body type'); |
| } |
| |
| const _toString = Object.prototype.toString; |
| |
| function toStringCheck(obj: any, name: string): boolean { |
| return _toString.call(obj) === `[object ${name}]`; |
| } |
| |
| function isArrayBuffer(body: any): body is ArrayBuffer { |
| return toStringCheck(body, 'ArrayBuffer'); |
| } |
| |
| function isFile(body: any): body is File { |
| return toStringCheck(body, 'File'); |
| } |
| |
| function isBlob(body: any): body is Blob { |
| return toStringCheck(body, 'Blob'); |
| } |
| |
| function isArrayBufferView(body: any): body is ArrayBufferView { |
| return typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView(body); |
| } |
| |
| function isFormData(body: any): body is FormData { |
| return typeof FormData !== 'undefined' && body instanceof FormData; |
| } |
| |
| function isURLSearchParams(body: any): body is URLSearchParams { |
| return typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams; |
| } |
| |
| function isReadableStream(body: any): body is ReadableStream { |
| return typeof ReadableStream !== 'undefined' && body instanceof ReadableStream; |
| } |