Feature/add default template parameters to web response (#24)

* Add Default template parameters to WebResponse
This commit is contained in:
Alexander T 2018-04-25 04:43:31 +03:00 коммит произвёл Brent Erickson
Родитель f422577529
Коммит 7336a53474
4 изменённых файлов: 106 добавлений и 71 удалений

Просмотреть файл

@ -8,8 +8,12 @@
import _ = require('lodash');
import SyncTasks = require('synctasks');
import { SimpleWebRequest, WebRequestOptions, WebResponse } from './SimpleWebRequest';
import {
WebRequestOptions,
WebResponse,
Headers,
SimpleWebRequest,
} from './SimpleWebRequest';
export type HttpAction = 'POST'|'GET'|'PUT'|'DELETE'|'PATCH';
@ -20,12 +24,10 @@ export interface ApiCallOptions extends WebRequestOptions {
}
export interface ETagResponse<T> {
// Indicates whether the provided ETag matched. If true,
// the response is undefined.
// Indicates whether the provided ETag matched. If true, the response is undefined.
eTagMatched?: boolean;
// If the ETag didn't match, the response contains the updated
// information.
// If the ETag didn't match, the response contains the updated information.
response?: T;
// The updated ETag value.
@ -33,12 +35,13 @@ export interface ETagResponse<T> {
}
export class GenericRestClient {
protected _endpointUrl: string;
protected _defaultOptions: ApiCallOptions = {
excludeEndpointUrl: false,
withCredentials: false,
retries: 0,
excludeEndpointUrl: false
};
constructor(endpointUrl: string) {
@ -46,22 +49,23 @@ export class GenericRestClient {
}
protected _performApiCall<T>(apiPath: string, action: HttpAction, objToPost: any, givenOptions?: ApiCallOptions)
: SyncTasks.Promise<WebResponse<T>> {
let options = _.defaults<ApiCallOptions, ApiCallOptions, ApiCallOptions>({}, givenOptions || {}, this._defaultOptions);
: SyncTasks.Promise<WebResponse<T, ApiCallOptions>> {
let options = _.defaults<ApiCallOptions, ApiCallOptions, ApiCallOptions>({}, givenOptions || {}, this._defaultOptions);
if (objToPost) {
options.sendData = objToPost;
}
let promise = this._blockRequestUntil(options);
const promise = this._blockRequestUntil(options);
if (!promise) {
return this._performApiCallInternal(apiPath, action, options);
}
return promise.then(() => this._performApiCallInternal(apiPath, action, options));
}
private _performApiCallInternal<T>(apiPath: string, action: HttpAction, options: ApiCallOptions)
: SyncTasks.Promise<WebResponse<T>> {
: SyncTasks.Promise<WebResponse<T, ApiCallOptions>> {
if (options.eTag) {
if (!options.augmentHeaders) {
@ -69,21 +73,22 @@ export class GenericRestClient {
}
options.augmentHeaders['If-None-Match'] = options.eTag;
}
if (!options.contentType) {
options.contentType = _.isString(options.sendData) ? 'form' : 'json';
}
const finalUrl = options.excludeEndpointUrl ? apiPath : this._endpointUrl + apiPath;
let request = new SimpleWebRequest<T>(action, finalUrl, options, () => this._getHeaders(options));
return request.start().then(resp => {
this._processSuccessResponse<T>(resp);
return resp;
});
return new SimpleWebRequest<T, ApiCallOptions>(action, finalUrl, options, () => this._getHeaders(options))
.start()
.then(response => {
this._processSuccessResponse<T>(response);
return response;
});
}
protected _getHeaders(options: ApiCallOptions): { [header: string]: string } {
protected _getHeaders(options: ApiCallOptions): Headers {
// Virtual function -- No-op by default
return {};
}
@ -95,42 +100,62 @@ export class GenericRestClient {
}
// Override this function to process any generic headers that come down with a successful response
protected _processSuccessResponse<T>(resp: WebResponse<T>): void {
protected _processSuccessResponse<T>(resp: WebResponse<T, ApiCallOptions>): void {
// No-op by default
}
performApiGet<T>(apiPath: string, options?: ApiCallOptions): SyncTasks.Promise<T> {
return this.performApiGetDetailed<T>(apiPath, options).then(resp => resp.body);
return this
.performApiGetDetailed<T>(apiPath, options)
.then(resp => resp.body);
}
performApiGetDetailed<T>(apiPath: string, options?: ApiCallOptions): SyncTasks.Promise<WebResponse<T>> {
performApiGetDetailed<T>(apiPath: string, options?: ApiCallOptions)
: SyncTasks.Promise<WebResponse<T, ApiCallOptions>> {
return this._performApiCall<T>(apiPath, 'GET', undefined, options);
}
performApiPost<T>(apiPath: string, objToPost: any, options?: ApiCallOptions): SyncTasks.Promise<T> {
return this.performApiPostDetailed<T>(apiPath, objToPost, options).then(resp => resp.body);
return this
.performApiPostDetailed<T>(apiPath, objToPost, options)
.then(resp => resp.body);
}
performApiPostDetailed<T>(apiPath: string, objToPost: any, options?: ApiCallOptions): SyncTasks.Promise<WebResponse<T>> {
performApiPostDetailed<T>(apiPath: string, objToPost: any, options?: ApiCallOptions)
: SyncTasks.Promise<WebResponse<T, ApiCallOptions>> {
return this._performApiCall<T>(apiPath, 'POST', objToPost, options);
}
performApiPatch<T>(apiPath: string, objToPatch: any, options?: ApiCallOptions): SyncTasks.Promise<T> {
return this.performApiPatchDetailed<T>(apiPath, objToPatch, options).then(resp => resp.body);
return this
.performApiPatchDetailed<T>(apiPath, objToPatch, options)
.then(resp => resp.body);
}
performApiPatchDetailed<T>(apiPath: string, objToPatch: any, options?: ApiCallOptions): SyncTasks.Promise<WebResponse<T>> {
performApiPatchDetailed<T>(apiPath: string, objToPatch: any, options?: ApiCallOptions)
: SyncTasks.Promise<WebResponse<T, ApiCallOptions>> {
return this._performApiCall<T>(apiPath, 'PATCH', objToPatch, options);
}
performApiPut<T>(apiPath: string, objToPut: any, options?: ApiCallOptions): SyncTasks.Promise<T> {
return this.performApiPutDetailed<T>(apiPath, objToPut, options).then(resp => resp.body);
return this
.performApiPutDetailed<T>(apiPath, objToPut, options)
.then(resp => resp.body);
}
performApiPutDetailed<T>(apiPath: string, objToPut: any, options?: ApiCallOptions): SyncTasks.Promise<WebResponse<T>> {
performApiPutDetailed<T>(apiPath: string, objToPut: any, options?: ApiCallOptions)
: SyncTasks.Promise<WebResponse<T, ApiCallOptions>> {
return this._performApiCall<T>(apiPath, 'PUT', objToPut, options);
}
performApiDelete<T>(apiPath: string, objToDelete?: any, options?: ApiCallOptions): SyncTasks.Promise<T> {
return this.performApiDeleteDetailed<T>(apiPath, objToDelete, options).then(resp => resp.body);
return this
.performApiDeleteDetailed<T>(apiPath, objToDelete, options)
.then(resp => resp.body);
}
performApiDeleteDetailed<T>(apiPath: string, objToDelete: any, options?: ApiCallOptions): SyncTasks.Promise<WebResponse<T>> {
performApiDeleteDetailed<T>(apiPath: string, objToDelete: any, options?: ApiCallOptions)
: SyncTasks.Promise<WebResponse<T, ApiCallOptions>> {
return this._performApiCall<T>(apiPath, 'DELETE', objToDelete, options);
}
}

Просмотреть файл

@ -12,16 +12,20 @@ import SyncTasks = require('synctasks');
import { ExponentialTime } from './ExponentialTime';
export interface Headers {
[header: string]: string;
}
export interface WebTransportResponseBase {
url: string;
method: string;
statusCode: number;
statusText: string|undefined;
headers: _.Dictionary<string>;
headers: Headers;
}
export interface WebTransportResponse<T> extends WebTransportResponseBase {
body: T;
export interface WebTransportResponse<TBody> extends WebTransportResponseBase {
body: TBody;
}
export interface WebTransportErrorResponse extends WebTransportResponseBase {
@ -30,19 +34,19 @@ export interface WebTransportErrorResponse extends WebTransportResponseBase {
timedOut: boolean;
}
export interface RestRequestInResponse {
requestOptions: WebRequestOptions;
requestHeaders: _.Dictionary<string>;
export interface RestRequestInResponse<TOptions = WebRequestOptions> {
requestOptions: TOptions;
requestHeaders: Headers;
}
export interface WebResponseBase extends RestRequestInResponse, WebTransportResponseBase {
}
export interface WebResponseBase<TOptions = WebRequestOptions>
extends WebTransportResponseBase, RestRequestInResponse<TOptions> {}
export interface WebResponse<T> extends RestRequestInResponse, WebTransportResponse<T> {
}
export interface WebErrorResponse<TOptions = WebRequestOptions>
extends WebTransportErrorResponse, RestRequestInResponse<TOptions> {}
export interface WebErrorResponse extends RestRequestInResponse, WebTransportErrorResponse {
}
export interface WebResponse<TBody, TOptions = WebRequestOptions>
extends WebTransportResponse<TBody>, RestRequestInResponse<TOptions> {}
export enum WebRequestPriority {
DontCare = 0,
@ -77,7 +81,7 @@ export interface NativeBlobFileData {
}
export interface NativeFileData {
file: NativeBlobFileData | File;
file: NativeBlobFileData|File;
}
export interface XMLHttpRequestProgressEvent extends ProgressEvent {
@ -100,12 +104,12 @@ export interface WebRequestOptions {
acceptType?: string;
contentType?: string;
sendData?: SendDataType;
/* Deprecated: use overrideGetHeaders */ headers?: _.Dictionary<string>;
/* Deprecated: use overrideGetHeaders */ headers?: Headers;
// Used instead of calling getHeaders.
overrideGetHeaders?: _.Dictionary<string>;
overrideGetHeaders?: Headers;
// Overrides all other headers.
augmentHeaders?: _.Dictionary<string>;
augmentHeaders?: Headers;
onProgress?: (progressEvent: XMLHttpRequestProgressEvent) => void;
@ -125,7 +129,7 @@ function isFormDataContentType(ct: string) {
return ct && ct.indexOf('multipart/form-data') === 0;
}
export let DefaultOptions: WebRequestOptions = {
export const DefaultOptions: WebRequestOptions = {
priority: WebRequestPriority.Normal
};
@ -147,7 +151,8 @@ export let SimpleWebRequestOptions: ISimpleWebRequestOptions = {
export function DefaultErrorHandler(webRequest: SimpleWebRequestBase, errResp: WebTransportErrorResponse) {
if (errResp.canceled || !errResp.statusCode || errResp.statusCode >= 400 && errResp.statusCode < 600) {
// Fail canceled/0/4xx/5xx requests immediately. These are permenent failures, and shouldn't have retry logic applied to them.
// Fail canceled/0/4xx/5xx requests immediately.
// These are permenent failures, and shouldn't have retry logic applied to them.
return ErrorHandlingType.DoNotRetry;
}
@ -173,11 +178,11 @@ let executingList: SimpleWebRequestBase[] = [];
let onLoadErrorSupportStatus = FeatureSupportStatus.Unknown;
let timeoutSupportStatus = FeatureSupportStatus.Unknown;
export abstract class SimpleWebRequestBase {
export abstract class SimpleWebRequestBase<TOptions extends WebRequestOptions = WebRequestOptions> {
protected _xhr: XMLHttpRequest|undefined;
protected _xhrRequestHeaders: _.Dictionary<string>|undefined;
protected _xhrRequestHeaders: Headers|undefined;
protected _requestTimeoutTimer: number|undefined;
protected _options: WebRequestOptions;
protected _options: TOptions;
protected _aborted = false;
protected _timedOut = false;
@ -191,8 +196,9 @@ export abstract class SimpleWebRequestBase {
protected _retryTimer: number|undefined;
protected _retryExponentialTime = new ExponentialTime(1000, 300000);
constructor(protected _action: string, protected _url: string, options: WebRequestOptions,
protected _getHeaders?: () => _.Dictionary<string>) {
constructor(protected _action: string,
protected _url: string, options: TOptions,
protected _getHeaders?: () => Headers) {
this._options = _.defaults(options, DefaultOptions);
}
@ -420,8 +426,8 @@ export abstract class SimpleWebRequestBase {
}
}
getRequestHeaders(): { [header: string]: string } {
let headers: { [header: string]: string } = {};
getRequestHeaders(): Headers {
let headers: Headers = {};
if (this._getHeaders && !this._options.overrideGetHeaders && !this._options.headers) {
headers = _.extend(headers, this._getHeaders());
@ -488,6 +494,7 @@ export abstract class SimpleWebRequestBase {
// Throw it on the queue
const index = _.findIndex(requestQueue, request =>
request.getPriority() < (this._options.priority || WebRequestPriority.DontCare));
if (index > -1) {
requestQueue.splice(index, 0, this);
} else {
@ -513,10 +520,11 @@ export abstract class SimpleWebRequestBase {
protected abstract _respond(errorStatusText?: string): void;
}
export class SimpleWebRequest<T> extends SimpleWebRequestBase {
private _deferred: SyncTasks.Deferred<WebResponse<T>>;
export class SimpleWebRequest<TBody, TOptions extends WebRequestOptions = WebRequestOptions> extends SimpleWebRequestBase<TOptions> {
constructor(action: string, url: string, options: WebRequestOptions, getHeaders?: () => _.Dictionary<string>) {
private _deferred: SyncTasks.Deferred<WebResponse<TBody, TOptions>>;
constructor(action: string, url: string, options: TOptions, getHeaders?: () => Headers) {
super(action, url, options, getHeaders);
}
@ -552,13 +560,13 @@ export class SimpleWebRequest<T> extends SimpleWebRequestBase {
}
}
start(): SyncTasks.Promise<WebResponse<T>> {
start(): SyncTasks.Promise<WebResponse<TBody, TOptions>> {
if (this._deferred) {
assert.ok(false, 'WebRequest already started');
return SyncTasks.Rejected('WebRequest already started');
}
this._deferred = SyncTasks.Defer<WebResponse<T>>();
this._deferred = SyncTasks.Defer<WebResponse<TBody, TOptions>>();
this._deferred.onCancel(() => {
// Abort the XHR -- this should chain through to the fail case on readystatechange
this.abort();
@ -611,7 +619,7 @@ export class SimpleWebRequest<T> extends SimpleWebRequestBase {
statusText = 'Browser Error - Possible CORS or Connectivity Issue';
}
let headers: _.Dictionary<string> = {};
let headers: Headers = {};
let body: any;
// Build the response info
@ -659,7 +667,7 @@ export class SimpleWebRequest<T> extends SimpleWebRequestBase {
if (this._xhr && this._xhr.readyState === 4 && ((statusCode >= 200 && statusCode < 300) || statusCode === 304)) {
// Happy path!
const resp: WebResponse<T> = {
const resp: WebResponse<TBody, TOptions> = {
url: this._xhr.responseURL || this._url,
method: this._action,
requestOptions: this._options,
@ -667,11 +675,12 @@ export class SimpleWebRequest<T> extends SimpleWebRequestBase {
statusCode: statusCode,
statusText: statusText,
headers: headers,
body: body as T
body: body as TBody,
};
this._deferred.resolve(resp);
} else {
let errResp: WebErrorResponse = {
let errResp: WebErrorResponse<TOptions> = {
url: (this._xhr ? this._xhr.responseURL : undefined) || this._url,
method: this._action,
requestOptions: this._options,
@ -681,7 +690,7 @@ export class SimpleWebRequest<T> extends SimpleWebRequestBase {
headers: headers,
body: body,
canceled: this._aborted,
timedOut: this._timedOut
timedOut: this._timedOut,
};
if (this._options.augmentErrorResponse) {
@ -689,8 +698,9 @@ export class SimpleWebRequest<T> extends SimpleWebRequestBase {
}
// Policy-adaptable failure
const handleResponse = this._options.customErrorHandler ? this._options.customErrorHandler(this, errResp) :
DefaultErrorHandler(this, errResp);
const handleResponse = this._options.customErrorHandler
? this._options.customErrorHandler(this, errResp)
: DefaultErrorHandler(this, errResp);
const retry = handleResponse !== ErrorHandlingType.DoNotRetry && (
(this._options.retries && this._options.retries > 0) ||

Просмотреть файл

@ -12,11 +12,11 @@
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true
},
"filesGlob": [
"src/**/*{ts,tsx}"
"include": [
"src/**/*"
],
"exclude": [
"dist",
"node_modules"

Просмотреть файл

@ -7,7 +7,7 @@
"forin": true,
"indent": [true, "spaces"],
"label-position": true,
"max-line-length": [ true, 140 ],
"max-line-length": [true, 140],
"no-arg": true,
"no-bitwise": false,
"no-console": [true,