Commit b304d412 authored by Winnie Hellmann's avatar Winnie Hellmann Committed by Clement Ho

Track pending requests in AjaxCache

parent cb2f739d
const AjaxCache = { class AjaxCache {
internalStorage: { }, constructor() {
this.internalStorage = { };
this.pendingRequests = { };
}
get(endpoint) { get(endpoint) {
return this.internalStorage[endpoint]; return this.internalStorage[endpoint];
}, }
hasData(endpoint) { hasData(endpoint) {
return Object.prototype.hasOwnProperty.call(this.internalStorage, endpoint); return Object.prototype.hasOwnProperty.call(this.internalStorage, endpoint);
}, }
purge(endpoint) {
remove(endpoint) {
delete this.internalStorage[endpoint]; delete this.internalStorage[endpoint];
}, }
retrieve(endpoint) { retrieve(endpoint) {
if (AjaxCache.hasData(endpoint)) { if (this.hasData(endpoint)) {
return Promise.resolve(AjaxCache.get(endpoint)); return Promise.resolve(this.get(endpoint));
} }
return new Promise((resolve, reject) => { let pendingRequest = this.pendingRequests[endpoint];
$.ajax(endpoint) // eslint-disable-line promise/catch-or-return
.then(data => resolve(data), if (!pendingRequest) {
(jqXHR, textStatus, errorThrown) => { pendingRequest = new Promise((resolve, reject) => {
const error = new Error(`${endpoint}: ${errorThrown}`); // jQuery 2 is not Promises/A+ compatible (missing catch)
error.textStatus = textStatus; $.ajax(endpoint) // eslint-disable-line promise/catch-or-return
reject(error); .then(data => resolve(data),
}, (jqXHR, textStatus, errorThrown) => {
); const error = new Error(`${endpoint}: ${errorThrown}`);
}) error.textStatus = textStatus;
.then((data) => { this.internalStorage[endpoint] = data; }) reject(error);
.then(() => AjaxCache.get(endpoint)); },
}, );
}; })
.then((data) => {
export default AjaxCache; this.internalStorage[endpoint] = data;
delete this.pendingRequests[endpoint];
})
.catch((error) => {
delete this.pendingRequests[endpoint];
throw error;
});
this.pendingRequests[endpoint] = pendingRequest;
}
return pendingRequest.then(() => this.get(endpoint));
}
}
export default new AjaxCache();
...@@ -5,19 +5,13 @@ describe('AjaxCache', () => { ...@@ -5,19 +5,13 @@ describe('AjaxCache', () => {
const dummyResponse = { const dummyResponse = {
important: 'dummy data', important: 'dummy data',
}; };
let ajaxSpy = (url) => {
expect(url).toBe(dummyEndpoint);
const deferred = $.Deferred();
deferred.resolve(dummyResponse);
return deferred.promise();
};
beforeEach(() => { beforeEach(() => {
AjaxCache.internalStorage = { }; AjaxCache.internalStorage = { };
spyOn(jQuery, 'ajax').and.callFake(url => ajaxSpy(url)); AjaxCache.pendingRequests = { };
}); });
describe('#get', () => { describe('get', () => {
it('returns undefined if cache is empty', () => { it('returns undefined if cache is empty', () => {
const data = AjaxCache.get(dummyEndpoint); const data = AjaxCache.get(dummyEndpoint);
...@@ -41,7 +35,7 @@ describe('AjaxCache', () => { ...@@ -41,7 +35,7 @@ describe('AjaxCache', () => {
}); });
}); });
describe('#hasData', () => { describe('hasData', () => {
it('returns false if cache is empty', () => { it('returns false if cache is empty', () => {
expect(AjaxCache.hasData(dummyEndpoint)).toBe(false); expect(AjaxCache.hasData(dummyEndpoint)).toBe(false);
}); });
...@@ -59,9 +53,9 @@ describe('AjaxCache', () => { ...@@ -59,9 +53,9 @@ describe('AjaxCache', () => {
}); });
}); });
describe('#purge', () => { describe('remove', () => {
it('does nothing if cache is empty', () => { it('does nothing if cache is empty', () => {
AjaxCache.purge(dummyEndpoint); AjaxCache.remove(dummyEndpoint);
expect(AjaxCache.internalStorage).toEqual({ }); expect(AjaxCache.internalStorage).toEqual({ });
}); });
...@@ -69,7 +63,7 @@ describe('AjaxCache', () => { ...@@ -69,7 +63,7 @@ describe('AjaxCache', () => {
it('does nothing if cache contains no matching data', () => { it('does nothing if cache contains no matching data', () => {
AjaxCache.internalStorage['not matching'] = dummyResponse; AjaxCache.internalStorage['not matching'] = dummyResponse;
AjaxCache.purge(dummyEndpoint); AjaxCache.remove(dummyEndpoint);
expect(AjaxCache.internalStorage['not matching']).toBe(dummyResponse); expect(AjaxCache.internalStorage['not matching']).toBe(dummyResponse);
}); });
...@@ -77,14 +71,27 @@ describe('AjaxCache', () => { ...@@ -77,14 +71,27 @@ describe('AjaxCache', () => {
it('removes matching data', () => { it('removes matching data', () => {
AjaxCache.internalStorage[dummyEndpoint] = dummyResponse; AjaxCache.internalStorage[dummyEndpoint] = dummyResponse;
AjaxCache.purge(dummyEndpoint); AjaxCache.remove(dummyEndpoint);
expect(AjaxCache.internalStorage).toEqual({ }); expect(AjaxCache.internalStorage).toEqual({ });
}); });
}); });
describe('#retrieve', () => { describe('retrieve', () => {
let ajaxSpy;
beforeEach(() => {
spyOn(jQuery, 'ajax').and.callFake(url => ajaxSpy(url));
});
it('stores and returns data from Ajax call if cache is empty', (done) => { it('stores and returns data from Ajax call if cache is empty', (done) => {
ajaxSpy = (url) => {
expect(url).toBe(dummyEndpoint);
const deferred = $.Deferred();
deferred.resolve(dummyResponse);
return deferred.promise();
};
AjaxCache.retrieve(dummyEndpoint) AjaxCache.retrieve(dummyEndpoint)
.then((data) => { .then((data) => {
expect(data).toBe(dummyResponse); expect(data).toBe(dummyResponse);
...@@ -94,6 +101,28 @@ describe('AjaxCache', () => { ...@@ -94,6 +101,28 @@ describe('AjaxCache', () => {
.catch(fail); .catch(fail);
}); });
it('makes no Ajax call if request is pending', () => {
const responseDeferred = $.Deferred();
ajaxSpy = (url) => {
expect(url).toBe(dummyEndpoint);
// neither reject nor resolve to keep request pending
return responseDeferred.promise();
};
const unexpectedResponse = data => fail(`Did not expect response: ${data}`);
AjaxCache.retrieve(dummyEndpoint)
.then(unexpectedResponse)
.catch(fail);
AjaxCache.retrieve(dummyEndpoint)
.then(unexpectedResponse)
.catch(fail);
expect($.ajax.calls.count()).toBe(1);
});
it('returns undefined if Ajax call fails and cache is empty', (done) => { it('returns undefined if Ajax call fails and cache is empty', (done) => {
const dummyStatusText = 'exploded'; const dummyStatusText = 'exploded';
const dummyErrorMessage = 'server exploded'; const dummyErrorMessage = 'server exploded';
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment