Skip to content

Instantly share code, notes, and snippets.

@mik01aj
Created March 17, 2015 11:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mik01aj/0e7fd661ef0ac8917181 to your computer and use it in GitHub Desktop.
Save mik01aj/0e7fd661ef0ac8917181 to your computer and use it in GitHub Desktop.
ModelStore - A simple tool to gather data requests from frontend to batch queries. See 2nd unit test for usage example.
/*jshint jasmine: true */
'use strict';
var ModelStore = require('../ModelStore');
var Promise = require('es6-promise').Promise;
var _ = require('lodash');
describe('ModelStore', function () {
var mockFunctionForTheThingApi, axiosGetSpy, store;
beforeEach(function () {
mockFunctionForTheThingApi = function(ids) {
return {
status: 200,
data: {
things: _.map(ids, function (id) {
return {id: id, hello: 'world'};
}),
},
};
};
// This is the fake function to handle HTTP requests
axiosGetSpy = jasmine.createSpy('axios.get').and.callFake(function (url) {
var ids = _.map(url.split('/things/')[1].split(','), _.parseInt);
return new Promise(function(fulfill, reject) {
fulfill(mockFunctionForTheThingApi(ids));
});
});
// Mocking stuff.
ModelStore.__set__('console.debug', function() {}); // disabling some console output
ModelStore.__set__('console.warn', function() {}); // disabling some console output
ModelStore.__set__('console.assert', function(condition, message) {
message = message || 'Assertion failed';
expect(condition).toBeTruthy(message);
}); // disabling console.logs
ModelStore.__set__('axios.get', axiosGetSpy); // mocking HTTP communication
// And this is the ModelStore we're going to test!
store = new ModelStore('http://example.com/things', 'things');
jasmine.clock().install();
});
afterEach(function () {
jasmine.clock().uninstall();
});
it('should resolve the promise when it receives a response', function () {
var promiseResolved = false;
store.get(123).then(function (thing) {
promiseResolved = true;
expect(thing).toEqual({id: 123, hello: 'world'});
}).catch(fail);
jasmine.clock().tick(1);
expect(promiseResolved).toBe(true);
});
it('should group requests for multiple ids', function () {
store.get(1);
store.get(2);
jasmine.clock().tick(1);
expect(axiosGetSpy).toHaveBeenCalledWith('http://example.com/things/1,2');
store.get(3);
store.get(4);
store.get(5);
jasmine.clock().tick(1);
expect(axiosGetSpy).toHaveBeenCalledWith('http://example.com/things/3,4,5');
});
it('should not request the same thing again', function () {
store.get(123);
jasmine.clock().tick(1);
expect(axiosGetSpy.calls.count()).toBe(1);
store.get(123);
jasmine.clock().tick(1);
expect(axiosGetSpy.calls.count()).toBe(1);
});
it('should not rely on the ordering in the result', function () {
mockFunctionForTheThingApi = function(ids) {
expect(ids).toEqual([1, 2]);
return {
status: 200,
data: {
things: [
{id: 2, hello: 'world'},
{id: 1, hello: 'world'},
],
},
};
};
var doneCount = 0;
store.get(1).then(function (result) {
expect(result).toEqual({id: 1, hello: 'world'});
doneCount++;
});
store.get(2).then(function (result) {
expect(result).toEqual({id: 2, hello: 'world'});
doneCount++;
});
jasmine.clock().tick(1);
expect(doneCount).toBe(2);
});
it('should handle missing results nicely', function () {
mockFunctionForTheThingApi = function(ids) {
expect(ids).toEqual([1, 2]);
return {
status: 200,
data: {
things: [
{id: 1, hello: 'world'},
],
},
};
};
var doneCount = 0;
store.get(1).then(function (result) {
expect(result).toEqual({id: 1, hello: 'world'});
doneCount++;
});
store.get(2).catch(function (err) {
expect(err).not.toBeNull();
doneCount++;
});
jasmine.clock().tick(1);
expect(doneCount).toBe(2);
});
});
'use strict';
var axios = require('axios');
var Promise = require('es6-promise').Promise;
var _ = require('lodash');
var ModelStore = function(endpointUrl, endpointType) {
this.endpointUrl = endpointUrl;
this.endpointType = endpointType;
this.store = {};
this.requested = {};
this.requestTimeout = null;
};
ModelStore.prototype.get = function(id) {
var that = this;
return new Promise(function (fulfill, reject) {
if (that.store[id]) {
fulfill(that.store[id]);
} else {
that._addRequest(id, fulfill, reject);
}
});
};
ModelStore.prototype._addRequest = function(id, fulfillCallback, rejectCallback) {
console.assert(fulfillCallback != null);
if (!this.requested[id]) {
this.requested[id] = [];
}
this.requested[id].push({fulfill: fulfillCallback, reject: rejectCallback});
if (!this.requestTimeout) {
this.requestTimeout = setTimeout(this.fetchRequested.bind(this), 0);
}
};
ModelStore.prototype.fetchRequested = function() {
this.requestTimeout = null;
var that = this;
var requested = this.requested;
this.requested = {};
var ids = _.keys(requested);
var url = this.endpointUrl + '/' + ids.join(',');
return axios.get(url).then(function(response) {
console.debug('ModelStore: ' + url + ' --> ', response);
var responseObjects = response.data[that.endpointType];
console.assert(responseObjects);
// fulfilling promises that could be fulfilled
_.each(responseObjects, function (receivedObj) {
console.assert(receivedObj);
var id = receivedObj.id;
console.assert(id in requested,
'Unexpected object with id=' + id + ': requested ' + ids);
that.store[id] = receivedObj; // Saving to cache
_.each(requested[id], function(promiseCallbacks) {
promiseCallbacks.fulfill(receivedObj);
});
delete requested[id];
});
// Sadly rejecting promises that could not be fulfilled
_.each(requested, function(promiseCallbacksList, id) {
_.each(promiseCallbacksList, function(promiseCallbacks) {
var message = ('The ' + that.endpointType + ' with id ' + id +
' was not present in the response at ' + url);
console.warn(message);
promiseCallbacks.reject(message);
});
});
}).catch(function (err) {
console.error('ModelStore: ' + url + ' --> ', err);
// TODO: if error is recoverable, retry the request
});
};
module.exports = ModelStore;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment