Skip to content

Commit 7066837

Browse files
mariarekberickson1
authored andcommitted
Make blockRequestUntil work with MaxSimultaneousRequests and handle e… (#35)
* Make blockRequestUntil work with MaxSimultaneousRequests and handle errors
1 parent 43f7349 commit 7066837

File tree

6 files changed

+270
-41
lines changed

6 files changed

+270
-41
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Changelog
2+
All notable changes to this project will be documented in this file.
3+
4+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5+
6+
## [0.2.0] - 2018-08-29
7+
### Added
8+
- Unit tests verifying queueing/blocking requests and executing them in order
9+
10+
### Changed
11+
- `GenericRestClient._blockRequestUntil` is now called every time when the request is on top of the pending requests queue rather than only once
12+
- If `GenericRestClient._blockRequestUntil` rejects, the whole request is properly rejected
13+
- All the `assert`s now properly clear the request queues before throwing
14+

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "simplerestclients",
3-
"version": "0.1.8",
3+
"version": "0.2.0",
44
"description": "A library of components for accessing RESTful services with javascript/typescript.",
55
"author": "David de Regt <David.de.Regt@microsoft.com>",
66
"scripts": {
@@ -9,7 +9,8 @@
99
"build": "npm run tslint && tsc",
1010
"tslint": "tslint --project tsconfig.json -r tslint.json --fix || true",
1111
"test": "npm run clean && karma start --singleRun",
12-
"test:watch": "npm run clean && karma start"
12+
"test:watch": "npm run clean && karma start",
13+
"test:browser": "npm run clean && karma start --browsers=Chrome --single-run=false --auto-watch"
1314
},
1415
"dependencies": {
1516
"@types/lodash": "^4.14.110",

src/GenericRestClient.ts

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,7 @@ export class GenericRestClient {
5050
if (objToPost) {
5151
options.sendData = objToPost;
5252
}
53-
54-
const promise = this._blockRequestUntil(options);
55-
if (!promise) {
56-
return this._performApiCallInternal(apiPath, action, options);
57-
}
58-
59-
return promise.then(() => this._performApiCallInternal(apiPath, action, options));
60-
}
61-
62-
private _performApiCallInternal<T>(apiPath: string, action: HttpAction, options: ApiCallOptions)
63-
: SyncTasks.Promise<WebResponse<T, ApiCallOptions>> {
64-
53+
6554
if (options.eTag) {
6655
if (!options.augmentHeaders) {
6756
options.augmentHeaders = {};
@@ -75,7 +64,8 @@ export class GenericRestClient {
7564

7665
const finalUrl = options.excludeEndpointUrl ? apiPath : this._endpointUrl + apiPath;
7766

78-
return new SimpleWebRequest<T, ApiCallOptions>(action, finalUrl, options, () => this._getHeaders(options))
67+
return new SimpleWebRequest<T, ApiCallOptions>(action, finalUrl, options, () => this._getHeaders(options),
68+
() => this._blockRequestUntil(options))
7969
.start()
8070
.then(response => {
8171
this._processSuccessResponse<T>(response);
@@ -89,6 +79,7 @@ export class GenericRestClient {
8979
}
9080

9181
// Override (but make sure to call super and chain appropriately) this function if you want to add more blocking criteria.
82+
// Also, this might be called multiple times to check if the conditions changed
9283
protected _blockRequestUntil(options: ApiCallOptions): SyncTasks.Promise<void>|undefined {
9384
// No-op by default
9485
return undefined;

src/SimpleWebRequest.ts

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ export abstract class SimpleWebRequestBase<TOptions extends WebRequestOptions =
189189
protected _aborted = false;
190190
protected _timedOut = false;
191191
protected _paused = false;
192+
protected _created = Date.now();
192193

193194
// De-dupe result handling for two reasons so far:
194195
// 1. Various platforms have bugs where they double-resolves aborted xmlhttprequests
@@ -200,7 +201,8 @@ export abstract class SimpleWebRequestBase<TOptions extends WebRequestOptions =
200201

201202
constructor(protected _action: string,
202203
protected _url: string, options: TOptions,
203-
protected _getHeaders?: () => Headers) {
204+
protected _getHeaders?: () => Headers,
205+
protected _blockRequestUntil?: () => SyncTasks.Promise<void>|undefined) {
204206
this._options = _.defaults(options, DefaultOptions);
205207
}
206208

@@ -213,8 +215,35 @@ export abstract class SimpleWebRequestBase<TOptions extends WebRequestOptions =
213215
protected static checkQueueProcessing() {
214216
while (requestQueue.length > 0 && executingList.length < SimpleWebRequestOptions.MaxSimultaneousRequests) {
215217
const req = requestQueue.shift()!!!;
216-
executingList.push(req);
217-
req._fire();
218+
const blockPromise = (req._blockRequestUntil && req._blockRequestUntil()) || SyncTasks.Resolved();
219+
blockPromise.then(() => {
220+
if (executingList.length < SimpleWebRequestOptions.MaxSimultaneousRequests) {
221+
executingList.push(req);
222+
req._fire();
223+
} else {
224+
req._enqueue();
225+
}
226+
}, err => {
227+
// fail the request if the block promise is rejected
228+
req._respond('_blockRequestUntil rejected: ' + err);
229+
});
230+
}
231+
}
232+
233+
protected _removeFromQueue(): void {
234+
// Pull it out of whichever queue it's sitting in
235+
if (this._xhr) {
236+
_.pull(executingList, this);
237+
} else {
238+
_.pull(requestQueue, this);
239+
}
240+
}
241+
242+
protected _assertAndClean(expression: any, message: string): void {
243+
if (!expression) {
244+
this._removeFromQueue();
245+
console.error(message);
246+
assert.ok(expression, message);
218247
}
219248
}
220249

@@ -240,7 +269,7 @@ export abstract class SimpleWebRequestBase<TOptions extends WebRequestOptions =
240269
const timeoutSupported = timeoutSupportStatus;
241270
// Use manual timer if we don't know about timeout support
242271
if (timeoutSupported !== FeatureSupportStatus.Supported) {
243-
assert.ok(!this._requestTimeoutTimer, 'Double-fired requestTimeoutTimer');
272+
this._assertAndClean(!this._requestTimeoutTimer, 'Double-fired requestTimeoutTimer');
244273
this._requestTimeoutTimer = SimpleWebRequestOptions.setTimeout(() => {
245274
this._requestTimeoutTimer = undefined;
246275

@@ -262,7 +291,7 @@ export abstract class SimpleWebRequestBase<TOptions extends WebRequestOptions =
262291
this._timedOut = true;
263292
// Set aborted flag to match simple timer approach, which aborts the request and results in an _respond call
264293
this._aborted = true;
265-
this._respond();
294+
this._respond('TimedOut');
266295
};
267296
}
268297
}
@@ -325,7 +354,7 @@ export abstract class SimpleWebRequestBase<TOptions extends WebRequestOptions =
325354
// so make sure we know that this is an abort.
326355
this._aborted = true;
327356

328-
this._respond();
357+
this._respond('Aborted');
329358
};
330359

331360
if (this._xhr.upload && this._options.onProgress) {
@@ -344,14 +373,14 @@ export abstract class SimpleWebRequestBase<TOptions extends WebRequestOptions =
344373
_.forEach(nextHeaders, (val, key) => {
345374
const headerLower = key.toLowerCase();
346375
if (headerLower === 'content-type') {
347-
assert.ok(false, 'Don\'t set Content-Type with options.headers -- use it with the options.contentType property');
376+
this._assertAndClean(false, 'Don\'t set Content-Type with options.headers -- use it with the options.contentType property');
348377
return;
349378
}
350379
if (headerLower === 'accept') {
351-
assert.ok(false, 'Don\'t set Accept with options.headers -- use it with the options.acceptType property');
380+
this._assertAndClean(false, 'Don\'t set Accept with options.headers -- use it with the options.acceptType property');
352381
return;
353382
}
354-
assert.ok(!headersCheck[headerLower], 'Setting duplicate header key: ' + headersCheck[headerLower] + ' and ' + key);
383+
this._assertAndClean(!headersCheck[headerLower], 'Setting duplicate header key: ' + headersCheck[headerLower] + ' and ' + key);
355384

356385
if (val === undefined || val === null) {
357386
console.warn('Tried to set header "' + key + '" on request with "' + val + '" value, header will be dropped');
@@ -499,11 +528,16 @@ export abstract class SimpleWebRequestBase<TOptions extends WebRequestOptions =
499528

500529
// Throw it on the queue
501530
const index = _.findIndex(requestQueue, request =>
502-
request.getPriority() < (this._options.priority || WebRequestPriority.DontCare));
531+
// find a request with the same priority, but newer
532+
(request.getPriority() === this.getPriority() && request._created > this._created) ||
533+
// or a request with lower priority
534+
(request.getPriority() < this.getPriority()));
503535

504536
if (index > -1) {
537+
//add me before the found request
505538
requestQueue.splice(index, 0, this);
506539
} else {
540+
//add me at the end
507541
requestQueue.push(this);
508542
}
509543

@@ -534,8 +568,9 @@ export class SimpleWebRequest<TBody, TOptions extends WebRequestOptions = WebReq
534568

535569
private _deferred: SyncTasks.Deferred<WebResponse<TBody, TOptions>>;
536570

537-
constructor(action: string, url: string, options: TOptions, getHeaders?: () => Headers) {
538-
super(action, url, options, getHeaders);
571+
constructor(action: string, url: string, options: TOptions, getHeaders?: () => Headers,
572+
blockRequestUntil?: () => SyncTasks.Promise<void>|undefined) {
573+
super(action, url, options, getHeaders, blockRequestUntil);
539574
}
540575

541576
abort(): void {
@@ -562,7 +597,7 @@ export class SimpleWebRequest<TBody, TOptions extends WebRequestOptions = WebReq
562597
}
563598

564599
// Cannot rely on this._xhr.abort() to trigger this._xhr.onAbort() synchronously, thus we must trigger an early response here
565-
this._respond();
600+
this._respond('Aborted');
566601

567602
if (this._xhr) {
568603
// Abort the in-flight request
@@ -599,12 +634,7 @@ export class SimpleWebRequest<TBody, TOptions extends WebRequestOptions = WebReq
599634

600635
this._finishHandled = true;
601636

602-
// Pull it out of whichever queue it's sitting in
603-
if (this._xhr) {
604-
_.pull(executingList, this as SimpleWebRequestBase);
605-
} else {
606-
_.pull(requestQueue, this as SimpleWebRequestBase);
607-
}
637+
this._removeFromQueue();
608638

609639
if (this._retryTimer) {
610640
SimpleWebRequestOptions.clearTimeout(this._retryTimer);
@@ -626,7 +656,7 @@ export class SimpleWebRequest<TBody, TOptions extends WebRequestOptions = WebReq
626656
// Some browsers error when you try to read status off aborted requests
627657
}
628658
} else {
629-
statusText = 'Browser Error - Possible CORS or Connectivity Issue';
659+
statusText = errorStatusText || 'Browser Error - Possible CORS or Connectivity Issue';
630660
}
631661

632662
let headers: Headers = {};

test/GenericRestClient.spec.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as faker from 'faker';
22
import { GenericRestClient, ApiCallOptions } from '../src/GenericRestClient';
33
import { DETAILED_RESPONSE, REQUEST_OPTIONS } from './helpers';
4+
import * as SyncTasks from 'synctasks';
45

56
class RestClient extends GenericRestClient { }
67
const BASE_URL = faker.internet.url();
@@ -317,7 +318,7 @@ describe('GenericRestClient', () => {
317318
expect(onSuccess).toHaveBeenCalledWith(response);
318319
});
319320

320-
it('performs request with custum headers', () => {
321+
it('performs request with custom headers', () => {
321322
const headers = {
322323
'Authorization': `Barrier ${faker.random.uuid()}`,
323324
};
@@ -370,4 +371,31 @@ describe('GenericRestClient', () => {
370371
expect(request.status).toEqual(statusCode);
371372
expect(onSuccess).toHaveBeenCalledWith(body.map((str: string) => str.trim()));
372373
});
374+
375+
it('blocks the request with custom method', () => {
376+
const blockDefer = SyncTasks.Defer<void>();
377+
378+
class Http extends GenericRestClient {
379+
protected _blockRequestUntil() {
380+
return blockDefer.promise();
381+
}
382+
}
383+
384+
const statusCode = 200;
385+
const onSuccess = jasmine.createSpy('onSuccess');
386+
const http = new Http(BASE_URL);
387+
const path = '/auth';
388+
389+
http.performApiGet(path)
390+
.then(onSuccess);
391+
392+
let request = jasmine.Ajax.requests.mostRecent();
393+
394+
expect(request).toBeUndefined();
395+
blockDefer.resolve(void 0);
396+
397+
request = jasmine.Ajax.requests.mostRecent();
398+
request.respondWith({ status: statusCode });
399+
expect(onSuccess).toHaveBeenCalled();
400+
});
373401
});

0 commit comments

Comments
 (0)