mirror of
https://github.com/swc-project/swc.git
synced 2024-12-27 15:42:51 +03:00
415 lines
11 KiB
TypeScript
415 lines
11 KiB
TypeScript
/** @prettier */
|
|
import { Observable } from '../../Observable';
|
|
import { Subscriber } from '../../Subscriber';
|
|
import { TeardownLogic, PartialObserver } from '../../types';
|
|
|
|
export interface AjaxRequest {
|
|
url?: string;
|
|
body?: any;
|
|
user?: string;
|
|
async?: boolean;
|
|
method?: string;
|
|
headers?: object;
|
|
timeout?: number;
|
|
password?: string;
|
|
hasContent?: boolean;
|
|
crossDomain?: boolean;
|
|
withCredentials?: boolean;
|
|
createXHR?: () => XMLHttpRequest;
|
|
progressSubscriber?: PartialObserver<ProgressEvent>;
|
|
responseType?: string;
|
|
}
|
|
|
|
function isFormData(body: any): body is FormData {
|
|
return typeof FormData !== 'undefined' && body instanceof FormData;
|
|
}
|
|
|
|
/**
|
|
* We need this JSDoc comment for affecting ESDoc.
|
|
* @extends {Ignored}
|
|
* @hide true
|
|
*/
|
|
export class AjaxObservable<T> extends Observable<T> {
|
|
private request: AjaxRequest;
|
|
|
|
constructor(urlOrRequest: string | AjaxRequest) {
|
|
super();
|
|
|
|
const request: AjaxRequest = {
|
|
async: true,
|
|
createXHR: () => new XMLHttpRequest(),
|
|
crossDomain: true,
|
|
withCredentials: false,
|
|
headers: {},
|
|
method: 'GET',
|
|
responseType: 'json',
|
|
timeout: 0,
|
|
};
|
|
|
|
if (typeof urlOrRequest === 'string') {
|
|
request.url = urlOrRequest;
|
|
} else {
|
|
for (const prop in urlOrRequest) {
|
|
if (urlOrRequest.hasOwnProperty(prop)) {
|
|
(request as any)[prop] = (urlOrRequest as any)[prop];
|
|
}
|
|
}
|
|
}
|
|
|
|
this.request = request;
|
|
}
|
|
|
|
/** @deprecated This is an internal implementation detail, do not use. */
|
|
_subscribe(subscriber: Subscriber<T>): TeardownLogic {
|
|
return new AjaxSubscriber(subscriber, this.request);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* We need this JSDoc comment for affecting ESDoc.
|
|
* @ignore
|
|
* @extends {Ignored}
|
|
*/
|
|
export class AjaxSubscriber<T> extends Subscriber<Event> {
|
|
// @ts-ignore: Property has no initializer and is not definitely assigned
|
|
private xhr: XMLHttpRequest;
|
|
private done: boolean = false;
|
|
|
|
constructor(destination: Subscriber<T>, public request: AjaxRequest) {
|
|
super(destination);
|
|
|
|
const headers = (request.headers = request.headers || {});
|
|
|
|
// force CORS if requested
|
|
if (!request.crossDomain && !this.getHeader(headers, 'X-Requested-With')) {
|
|
(headers as any)['X-Requested-With'] = 'XMLHttpRequest';
|
|
}
|
|
|
|
// ensure content type is set
|
|
let contentTypeHeader = this.getHeader(headers, 'Content-Type');
|
|
if (!contentTypeHeader && typeof request.body !== 'undefined' && !isFormData(request.body)) {
|
|
(headers as any)['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
|
|
}
|
|
|
|
// properly serialize body
|
|
request.body = this.serializeBody(request.body, this.getHeader(request.headers, 'Content-Type'));
|
|
|
|
this.send();
|
|
}
|
|
|
|
next(e: Event): void {
|
|
this.done = true;
|
|
const destination = this.destination as Subscriber<any>;
|
|
let result: AjaxResponse;
|
|
try {
|
|
result = new AjaxResponse(e, this.xhr, this.request);
|
|
} catch (err) {
|
|
return destination.error(err);
|
|
}
|
|
destination.next(result);
|
|
}
|
|
|
|
private send(): void {
|
|
const {
|
|
request,
|
|
request: { user, method, url, async, password, headers, body },
|
|
} = this;
|
|
try {
|
|
const xhr = (this.xhr = request.createXHR!());
|
|
|
|
// 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.
|
|
this.setupEvents(xhr, request);
|
|
// open XHR
|
|
if (user) {
|
|
xhr.open(method!, url!, async!, user, 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 as any;
|
|
}
|
|
|
|
if ('withCredentials' in xhr) {
|
|
xhr.withCredentials = !!request.withCredentials;
|
|
}
|
|
|
|
// set headers
|
|
this.setHeaders(xhr, headers!);
|
|
|
|
// finally send the request
|
|
if (body) {
|
|
xhr.send(body);
|
|
} else {
|
|
xhr.send();
|
|
}
|
|
} catch (err) {
|
|
this.error(err);
|
|
}
|
|
}
|
|
|
|
private serializeBody(body: any, contentType?: string) {
|
|
if (!body || typeof body === 'string') {
|
|
return body;
|
|
} else if (isFormData(body)) {
|
|
return body;
|
|
}
|
|
|
|
if (contentType) {
|
|
const splitIndex = contentType.indexOf(';');
|
|
if (splitIndex !== -1) {
|
|
contentType = contentType.substring(0, splitIndex);
|
|
}
|
|
}
|
|
|
|
switch (contentType) {
|
|
case 'application/x-www-form-urlencoded':
|
|
return Object.keys(body)
|
|
.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(body[key])}`)
|
|
.join('&');
|
|
case 'application/json':
|
|
return JSON.stringify(body);
|
|
default:
|
|
return body;
|
|
}
|
|
}
|
|
|
|
private setHeaders(xhr: XMLHttpRequest, headers: Object) {
|
|
for (let key in headers) {
|
|
if (headers.hasOwnProperty(key)) {
|
|
xhr.setRequestHeader(key, (headers as any)[key]);
|
|
}
|
|
}
|
|
}
|
|
|
|
private getHeader(headers: {}, headerName: string): any {
|
|
for (let key in headers) {
|
|
if (key.toLowerCase() === headerName.toLowerCase()) {
|
|
return (headers as any)[key];
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
private setupEvents(xhr: XMLHttpRequest, request: AjaxRequest) {
|
|
const progressSubscriber = request.progressSubscriber;
|
|
|
|
xhr.ontimeout = (e: ProgressEvent) => {
|
|
progressSubscriber?.error?.(e);
|
|
let error;
|
|
try {
|
|
error = new AjaxTimeoutError(xhr, request); // TODO: Make better.
|
|
} catch (err) {
|
|
error = err;
|
|
}
|
|
this.error(error);
|
|
};
|
|
|
|
if (progressSubscriber) {
|
|
xhr.upload.onprogress = (e: ProgressEvent) => {
|
|
progressSubscriber.next?.(e);
|
|
};
|
|
}
|
|
|
|
xhr.onerror = (e: ProgressEvent) => {
|
|
progressSubscriber?.error?.(e);
|
|
this.error(new AjaxError('ajax error', xhr, request));
|
|
};
|
|
|
|
xhr.onload = (e: ProgressEvent) => {
|
|
// 4xx and 5xx should error (https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)
|
|
if (xhr.status < 400) {
|
|
progressSubscriber?.complete?.();
|
|
this.next(e);
|
|
this.complete();
|
|
} else {
|
|
progressSubscriber?.error?.(e);
|
|
let error;
|
|
try {
|
|
error = new AjaxError('ajax error ' + xhr.status, xhr, request);
|
|
} catch (err) {
|
|
error = err;
|
|
}
|
|
this.error(error);
|
|
}
|
|
};
|
|
}
|
|
|
|
unsubscribe() {
|
|
const { done, xhr } = this;
|
|
if (!done && xhr && xhr.readyState !== 4 && typeof xhr.abort === 'function') {
|
|
xhr.abort();
|
|
}
|
|
super.unsubscribe();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A normalized AJAX response.
|
|
*
|
|
* @see {@link ajax}
|
|
*
|
|
* @class AjaxResponse
|
|
*/
|
|
export class AjaxResponse {
|
|
/** @type {number} The HTTP status code */
|
|
status: number;
|
|
|
|
/** @type {string|ArrayBuffer|Document|object|any} The response data */
|
|
response: any;
|
|
|
|
/** @type {string} The raw responseText */
|
|
// @ts-ignore: Property has no initializer and is not definitely assigned
|
|
responseText: string;
|
|
|
|
/** @type {string} The responseType (e.g. 'json', 'arraybuffer', or 'xml') */
|
|
responseType: string;
|
|
|
|
constructor(public originalEvent: Event, public xhr: XMLHttpRequest, public request: AjaxRequest) {
|
|
this.status = xhr.status;
|
|
this.responseType = xhr.responseType || request.responseType!;
|
|
this.response = getXHRResponse(xhr);
|
|
}
|
|
}
|
|
|
|
export type AjaxErrorNames = 'AjaxError' | 'AjaxTimeoutError';
|
|
|
|
/**
|
|
* A normalized AJAX error.
|
|
*
|
|
* @see {@link ajax}
|
|
*
|
|
* @class AjaxError
|
|
*/
|
|
export interface AjaxError extends Error {
|
|
/**
|
|
* The XHR instance associated with the error
|
|
*/
|
|
xhr: XMLHttpRequest;
|
|
|
|
/**
|
|
* The AjaxRequest associated with the error
|
|
*/
|
|
request: AjaxRequest;
|
|
|
|
/**
|
|
*The HTTP status code
|
|
*/
|
|
status: number;
|
|
|
|
/**
|
|
*The responseType (e.g. 'json', 'arraybuffer', or 'xml')
|
|
*/
|
|
responseType: XMLHttpRequestResponseType;
|
|
|
|
/**
|
|
* The response data
|
|
*/
|
|
response: any;
|
|
}
|
|
|
|
export interface AjaxErrorCtor {
|
|
/**
|
|
* Internal use only. Do not manually create instances of this type.
|
|
* @internal
|
|
*/
|
|
new (message: string, xhr: XMLHttpRequest, request: AjaxRequest): AjaxError;
|
|
}
|
|
|
|
const AjaxErrorImpl = (() => {
|
|
function AjaxErrorImpl(this: any, message: string, xhr: XMLHttpRequest, request: AjaxRequest): AjaxError {
|
|
Error.call(this);
|
|
this.message = message;
|
|
this.name = 'AjaxError';
|
|
this.xhr = xhr;
|
|
this.request = request;
|
|
this.status = xhr.status;
|
|
this.responseType = xhr.responseType;
|
|
let response: any;
|
|
try {
|
|
response = getXHRResponse(xhr);
|
|
} catch (err) {
|
|
response = xhr.responseText;
|
|
}
|
|
this.response = response;
|
|
return this;
|
|
}
|
|
AjaxErrorImpl.prototype = Object.create(Error.prototype);
|
|
return AjaxErrorImpl;
|
|
})();
|
|
|
|
/**
|
|
* Thrown when an error occurs during an AJAX request.
|
|
* This is only exported because it is useful for checking to see if an error
|
|
* is an `instanceof AjaxError`. DO NOT create new instances of `AjaxError` with
|
|
* the constructor.
|
|
*
|
|
* @class AjaxError
|
|
* @see ajax
|
|
*/
|
|
export const AjaxError: AjaxErrorCtor = AjaxErrorImpl as any;
|
|
|
|
function getXHRResponse(xhr: XMLHttpRequest) {
|
|
switch (xhr.responseType) {
|
|
case 'json': {
|
|
if ('response' in xhr) {
|
|
return xhr.response;
|
|
} else {
|
|
// IE
|
|
const ieXHR: any = xhr;
|
|
return JSON.parse(ieXHR.responseText);
|
|
}
|
|
}
|
|
case 'document':
|
|
return xhr.responseXML;
|
|
case 'text':
|
|
default: {
|
|
if ('response' in xhr) {
|
|
return xhr.response;
|
|
} else {
|
|
// IE
|
|
const ieXHR: any = xhr;
|
|
return ieXHR.responseText;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export interface AjaxTimeoutError extends AjaxError {}
|
|
|
|
export interface AjaxTimeoutErrorCtor {
|
|
/**
|
|
* Internal use only. Do not manually create instances of this type.
|
|
* @internal
|
|
*/
|
|
new (xhr: XMLHttpRequest, request: AjaxRequest): AjaxTimeoutError;
|
|
}
|
|
|
|
const AjaxTimeoutErrorImpl = (() => {
|
|
function AjaxTimeoutErrorImpl(this: any, xhr: XMLHttpRequest, request: AjaxRequest) {
|
|
AjaxError.call(this, 'ajax timeout', xhr, request);
|
|
this.name = 'AjaxTimeoutError';
|
|
return this;
|
|
}
|
|
AjaxTimeoutErrorImpl.prototype = Object.create(AjaxError.prototype);
|
|
return AjaxTimeoutErrorImpl;
|
|
})();
|
|
|
|
/**
|
|
* Thrown when an AJAX request timeout. Not to be confused with {@link TimeoutError}.
|
|
*
|
|
* This is exported only because it is useful for checking to see if errors are an
|
|
* `instanceof AjaxTimeoutError`. DO NOT use the constructor to create an instance of
|
|
* this type.
|
|
*
|
|
* @class AjaxTimeoutError
|
|
* @see ajax
|
|
*/
|
|
export const AjaxTimeoutError: AjaxTimeoutErrorCtor = AjaxTimeoutErrorImpl as any;
|