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

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

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

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

@ -13,8 +13,8 @@
"forceConsistentCasingInFileNames": true "forceConsistentCasingInFileNames": true
}, },
"filesGlob": [ "include": [
"src/**/*{ts,tsx}" "src/**/*"
], ],
"exclude": [ "exclude": [

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

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