Merge branch 'winh-pending-ajax-cache' into 'master'
Track pending requests in AjaxCache See merge request !11170
This commit is contained in:
commit
4086fca0f4
|
@ -1,32 +1,54 @@
|
||||||
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),
|
|
||||||
(jqXHR, textStatus, errorThrown) => {
|
|
||||||
const error = new Error(`${endpoint}: ${errorThrown}`);
|
|
||||||
error.textStatus = textStatus;
|
|
||||||
reject(error);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.then((data) => { this.internalStorage[endpoint] = data; })
|
|
||||||
.then(() => AjaxCache.get(endpoint));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AjaxCache;
|
if (!pendingRequest) {
|
||||||
|
pendingRequest = new Promise((resolve, reject) => {
|
||||||
|
// jQuery 2 is not Promises/A+ compatible (missing catch)
|
||||||
|
$.ajax(endpoint) // eslint-disable-line promise/catch-or-return
|
||||||
|
.then(data => resolve(data),
|
||||||
|
(jqXHR, textStatus, errorThrown) => {
|
||||||
|
const error = new Error(`${endpoint}: ${errorThrown}`);
|
||||||
|
error.textStatus = textStatus;
|
||||||
|
reject(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
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', () => {
|
||||||
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', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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', () => {
|
||||||
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', () => {
|
||||||
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', () => {
|
||||||
.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';
|
||||||
|
|
Loading…
Reference in New Issue