Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds request queueing to API resource fetch, adds save method to Models. #279

Closed
wants to merge 6 commits into from
Closed
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 133 additions & 52 deletions kolibri/core/assets/src/api_resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,35 +28,106 @@ class Model {

// force IDs to always be strings - this should be changed on the server-side too
this.attributes[this.resource.idKey] = String(this.attributes[this.resource.idKey]);

This comment was marked as spam.


// Keep track of any unresolved promises that have been generated by async methods of the Model
this.promises = [];
}

/**
* Method to fetch data from the server for this particular model.
* @param {object} params - an object of parameters to be parsed into GET parameters on the
* fetch.
* @param {boolean} force - fetch whether or not it's been synced already.
* @param {boolean} [force=false] - fetch whether or not it's been synced already.
* @returns {Promise} - Promise is resolved with Model attributes when the XHR successfully
* returns, otherwise reject is called with the response object.
*/
fetch(params = {}, force = false) {
if (!force && this.synced) {
return Promise.resolve(this.attributes);
}
this.synced = false;
return new Promise((resolve, reject) => {
// Do a fetch on the URL.
client({ path: this.url, params }).then((response) => {
// Set the retrieved Object onto the Model instance.
this.set(response.entity);
// Flag that the Model has been fetched.
this.synced = true;
// Resolve the promise with the attributes of the Model.
resolve(this.attributes);
}, (response) => {
logging.error('An error occurred', response);
reject(response);
const promise = new Promise((resolve, reject) => {

This comment was marked as spam.

Promise.all(this.promises).then(() => {
if (!force && this.synced) {

This comment was marked as spam.

resolve(this.attributes);
} else {
this.synced = false;
// Do a fetch on the URL.
client({ path: this.url, params }).then((response) => {
// Set the retrieved Object onto the Model instance.
this.set(response.entity);
// Flag that the Model has been fetched.
this.synced = true;
// Resolve the promise with the attributes of the Model.
resolve(this.attributes);
// Clean up the reference to this promise
this.promises.splice(this.promises.indexOf(promise), 1);
}, (response) => {
logging.error('An error occurred', response);

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

reject(response);
// Clean up the reference to this promise
this.promises.splice(this.promises.indexOf(promise), 1);
});
}
});

This comment was marked as spam.

This comment was marked as spam.

});
this.promises.push(promise);
return promise;
}

/**
* Method to save data to the server for this particular model.
* @param {object} attrs - an object of attributes to be saved on the model.
* @returns {Promise} - Promise is resolved with Model attributes when the XHR successfully
* returns, otherwise reject is called with the response object.
*/
save(attrs) {
const promise = new Promise((resolve, reject) => {
Promise.all(this.promises).then(() => {
let payload = {};
if (this.synced) {
// Model is synced with the server, so we can do dirty checking.
Object.keys(attrs).forEach((key) => {
if (attrs[key] !== this.attributes[key]) {

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

payload[key] = attrs[key];
}
});
} else {
payload = attrs;

This comment was marked as spam.

}
if (!Object.keys(payload).length) {
// Nothing to save, so just resolve the promise now.
resolve(this.attributes);
} else {
this.synced = false;
let url;
let method;
if (this.id) {
// If this Model has an id, then can do a PATCH against the Model
url = this.url;
method = 'PATCH';
} else {
// Otherwise, must POST to the Collection endpoint to create the Model
url = this.resource.collectionUrl();
method = 'POST';
}
// Do a save on the URL.
client({ path: url, method }).then((response) => {

This comment was marked as spam.

This comment was marked as spam.

// Set the retrieved Object onto the Model instance.
this.set(response.entity);
// Flag that the Model has been fetched.
this.synced = true;
// Resolve the promise with the attributes of the Model.
resolve(this.attributes);
// Clean up the reference to this promise
this.promises.splice(this.promises.indexOf(promise), 1);
}, (response) => {
logging.error('An error occurred', response);
reject(response);
// Clean up the reference to this promise
this.promises.splice(this.promises.indexOf(promise), 1);
});
}
});
});
this.promises.push(promise);
return promise;
}

get url() {
Expand Down Expand Up @@ -94,6 +165,8 @@ class Collection {
this._model_map = {};
this.synced = false;
this.set(data);
// Keep track of any unresolved promises that have been generated by async methods of the Model
this.promises = [];
}

/**
Expand All @@ -105,46 +178,54 @@ class Collection {
* successfully returns, otherwise reject is called with the response object.
*/
fetch(extraParams = {}, force = false) {
if (!force && this.synced) {
return Promise.resolve(this.data);
}
this.synced = false;
const params = Object.assign({}, this.params, extraParams);
return new Promise((resolve, reject) => {
// Do a fetch on the URL, with the parameters passed in.
client({ path: this.url, params }).then((response) => {
// Reset current models to only include ones from this fetch.
this.models = [];
this._model_map = {};
// Set response object - an Array - on the Collection to record the data.
// First check that the response *is* an Array
if (Array.isArray(response.entity)) {
this.set(response.entity);
const promise = new Promise((resolve, reject) => {

This comment was marked as spam.

Promise.all(this.promises).then(() => {
if (!force && this.synced) {
resolve(this.data);
} else {
// If it's not, there are two possibilities - something is awry, or we have received
// paginated data! Check to see if it is paginated.
if (typeof response.entity.results !== 'undefined') {
// Paginated objects have 'results' as their results object so interpret this as
// such.
this.set(response.entity.results);
this.pageCount = Math.ceil(response.entity.count / this.pageSize);
} else {
// It's all gone a bit Pete Tong.
logging.debug('Data appears to be malformed', response.entity);
}
this.synced = false;
client({ path: this.url, params }).then((response) => {
// Reset current models to only include ones from this fetch.
this.models = [];
this._model_map = {};
// Set response object - an Array - on the Collection to record the data.
// First check that the response *is* an Array
if (Array.isArray(response.entity)) {
this.set(response.entity);
} else {
// If it's not, there are two possibilities - something is awry, or we have received
// paginated data! Check to see if it is paginated.
if (typeof response.entity.results !== 'undefined') {
// Paginated objects have 'results' as their results object so interpret this as
// such.
this.set(response.entity.results);
this.pageCount = Math.ceil(response.entity.count / this.pageSize);
} else {
// It's all gone a bit Pete Tong.
logging.debug('Data appears to be malformed', response.entity);
}
}
// Mark that the fetch has completed.
this.synced = true;
this.models.forEach((model) => {
model.synced = true; // eslint-disable-line no-param-reassign
});
// Return the data from the models, not the models themselves.
resolve(this.data);
// Clean up the reference to this promise
this.promises.splice(this.promises.indexOf(promise), 1);
}, (response) => {
logging.error('An error occurred', response);
reject(response);
// Clean up the reference to this promise
this.promises.splice(this.promises.indexOf(promise), 1);
});
}
// Mark that the fetch has completed.
this.synced = true;
this.models.forEach((model) => {
model.synced = true; // eslint-disable-line no-param-reassign
});
// Return the data from the models, not the models themselves.
resolve(this.data);
}, (response) => {
logging.error('An error occurred', response);
reject(response);
});
});
this.promises.push(promise);
return promise;
}

get url() {
Expand Down