diff --git a/src/GenericRestClient.ts b/src/GenericRestClient.ts index 4fe69b5..aadb92a 100644 --- a/src/GenericRestClient.ts +++ b/src/GenericRestClient.ts @@ -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 { - // 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 { } 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(apiPath: string, action: HttpAction, objToPost: any, givenOptions?: ApiCallOptions) - : SyncTasks.Promise> { - let options = _.defaults({}, givenOptions || {}, this._defaultOptions); + : SyncTasks.Promise> { + let options = _.defaults({}, 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(apiPath: string, action: HttpAction, options: ApiCallOptions) - : SyncTasks.Promise> { + : SyncTasks.Promise> { 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(action, finalUrl, options, () => this._getHeaders(options)); - return request.start().then(resp => { - this._processSuccessResponse(resp); - return resp; - }); + return new SimpleWebRequest(action, finalUrl, options, () => this._getHeaders(options)) + .start() + .then(response => { + this._processSuccessResponse(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(resp: WebResponse): void { + protected _processSuccessResponse(resp: WebResponse): void { // No-op by default } performApiGet(apiPath: string, options?: ApiCallOptions): SyncTasks.Promise { - return this.performApiGetDetailed(apiPath, options).then(resp => resp.body); + return this + .performApiGetDetailed(apiPath, options) + .then(resp => resp.body); } - performApiGetDetailed(apiPath: string, options?: ApiCallOptions): SyncTasks.Promise> { + + performApiGetDetailed(apiPath: string, options?: ApiCallOptions) + : SyncTasks.Promise> { return this._performApiCall(apiPath, 'GET', undefined, options); } performApiPost(apiPath: string, objToPost: any, options?: ApiCallOptions): SyncTasks.Promise { - return this.performApiPostDetailed(apiPath, objToPost, options).then(resp => resp.body); + return this + .performApiPostDetailed(apiPath, objToPost, options) + .then(resp => resp.body); } - performApiPostDetailed(apiPath: string, objToPost: any, options?: ApiCallOptions): SyncTasks.Promise> { + + performApiPostDetailed(apiPath: string, objToPost: any, options?: ApiCallOptions) + : SyncTasks.Promise> { return this._performApiCall(apiPath, 'POST', objToPost, options); } performApiPatch(apiPath: string, objToPatch: any, options?: ApiCallOptions): SyncTasks.Promise { - return this.performApiPatchDetailed(apiPath, objToPatch, options).then(resp => resp.body); + return this + .performApiPatchDetailed(apiPath, objToPatch, options) + .then(resp => resp.body); } - performApiPatchDetailed(apiPath: string, objToPatch: any, options?: ApiCallOptions): SyncTasks.Promise> { + + performApiPatchDetailed(apiPath: string, objToPatch: any, options?: ApiCallOptions) + : SyncTasks.Promise> { return this._performApiCall(apiPath, 'PATCH', objToPatch, options); } performApiPut(apiPath: string, objToPut: any, options?: ApiCallOptions): SyncTasks.Promise { - return this.performApiPutDetailed(apiPath, objToPut, options).then(resp => resp.body); + return this + .performApiPutDetailed(apiPath, objToPut, options) + .then(resp => resp.body); } - performApiPutDetailed(apiPath: string, objToPut: any, options?: ApiCallOptions): SyncTasks.Promise> { + + performApiPutDetailed(apiPath: string, objToPut: any, options?: ApiCallOptions) + : SyncTasks.Promise> { return this._performApiCall(apiPath, 'PUT', objToPut, options); } performApiDelete(apiPath: string, objToDelete?: any, options?: ApiCallOptions): SyncTasks.Promise { - return this.performApiDeleteDetailed(apiPath, objToDelete, options).then(resp => resp.body); + return this + .performApiDeleteDetailed(apiPath, objToDelete, options) + .then(resp => resp.body); } - performApiDeleteDetailed(apiPath: string, objToDelete: any, options?: ApiCallOptions): SyncTasks.Promise> { + + performApiDeleteDetailed(apiPath: string, objToDelete: any, options?: ApiCallOptions) + : SyncTasks.Promise> { return this._performApiCall(apiPath, 'DELETE', objToDelete, options); } } diff --git a/src/SimpleWebRequest.ts b/src/SimpleWebRequest.ts index 5cad5d0..9d32a43 100644 --- a/src/SimpleWebRequest.ts +++ b/src/SimpleWebRequest.ts @@ -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; + headers: Headers; } -export interface WebTransportResponse extends WebTransportResponseBase { - body: T; +export interface WebTransportResponse 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; +export interface RestRequestInResponse { + requestOptions: TOptions; + requestHeaders: Headers; } -export interface WebResponseBase extends RestRequestInResponse, WebTransportResponseBase { -} +export interface WebResponseBase + extends WebTransportResponseBase, RestRequestInResponse {} -export interface WebResponse extends RestRequestInResponse, WebTransportResponse { -} +export interface WebErrorResponse + extends WebTransportErrorResponse, RestRequestInResponse {} -export interface WebErrorResponse extends RestRequestInResponse, WebTransportErrorResponse { -} +export interface WebResponse + extends WebTransportResponse, RestRequestInResponse {} 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; + /* Deprecated: use overrideGetHeaders */ headers?: Headers; // Used instead of calling getHeaders. - overrideGetHeaders?: _.Dictionary; + overrideGetHeaders?: Headers; // Overrides all other headers. - augmentHeaders?: _.Dictionary; + 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 { protected _xhr: XMLHttpRequest|undefined; - protected _xhrRequestHeaders: _.Dictionary|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) { + 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 extends SimpleWebRequestBase { - private _deferred: SyncTasks.Deferred>; +export class SimpleWebRequest extends SimpleWebRequestBase { - constructor(action: string, url: string, options: WebRequestOptions, getHeaders?: () => _.Dictionary) { + private _deferred: SyncTasks.Deferred>; + + constructor(action: string, url: string, options: TOptions, getHeaders?: () => Headers) { super(action, url, options, getHeaders); } @@ -552,13 +560,13 @@ export class SimpleWebRequest extends SimpleWebRequestBase { } } - start(): SyncTasks.Promise> { + start(): SyncTasks.Promise> { if (this._deferred) { assert.ok(false, 'WebRequest already started'); return SyncTasks.Rejected('WebRequest already started'); } - this._deferred = SyncTasks.Defer>(); + this._deferred = SyncTasks.Defer>(); 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 extends SimpleWebRequestBase { statusText = 'Browser Error - Possible CORS or Connectivity Issue'; } - let headers: _.Dictionary = {}; + let headers: Headers = {}; let body: any; // Build the response info @@ -659,7 +667,7 @@ export class SimpleWebRequest extends SimpleWebRequestBase { if (this._xhr && this._xhr.readyState === 4 && ((statusCode >= 200 && statusCode < 300) || statusCode === 304)) { // Happy path! - const resp: WebResponse = { + const resp: WebResponse = { url: this._xhr.responseURL || this._url, method: this._action, requestOptions: this._options, @@ -667,11 +675,12 @@ export class SimpleWebRequest 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 = { url: (this._xhr ? this._xhr.responseURL : undefined) || this._url, method: this._action, requestOptions: this._options, @@ -681,7 +690,7 @@ export class SimpleWebRequest 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 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) || diff --git a/tsconfig.json b/tsconfig.json index 4c5f70f..17eef85 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,11 +12,11 @@ "strictNullChecks": true, "forceConsistentCasingInFileNames": true }, - - "filesGlob": [ - "src/**/*{ts,tsx}" + + "include": [ + "src/**/*" ], - + "exclude": [ "dist", "node_modules" diff --git a/tslint.json b/tslint.json index f4e541d..7e41efe 100644 --- a/tslint.json +++ b/tslint.json @@ -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,