@prorenata/vue-rest-resource
Version:
Rest resource management for Vue.js and Vuex projects
285 lines (231 loc) • 8.65 kB
JavaScript
import axios from 'axios';
import Http from './Http';
import Subscriber from './subscriber';
import componentRegisterMap from './componentRegisterMap';
const capitalizeFirst = (str) => str.charAt(0).toUpperCase() + str.slice(1);
const getRequestSignature = (req) => {
const params = Object.keys(req.params || []).filter((param) => param[0] !== '_');
return `${req.endpoint}_${(params || []).join('&')}`;
};
/*
* Global Queue has the purpose of preventing N requests being sent in a row to same endpoint.
*
* If 1 request is pending to a specific endpoint a success result will be applied to all
* queued requests, without them having to be fired to server.
*
* All requests gets registered in store as pending, so we can track they existed.
* We add a prop .debouncedResponse with value: null - if the request got its own
* response; Object - the request object of the request that got the response data
*
* Not implemented yet:
*
* If a "update" request gets in between 2 get requests, the earlier "get" will be
* postponed, we send the "update" to server and apply its response to both "get"s.
*
*/
const globalQueue = {
activeRequests: {}, // endpoints as key values
queuedRequests: {}, // endpoints as key values
};
const handleQueueOnBadRequest = (req) => {
const signature = getRequestSignature(req);
delete globalQueue.activeRequests[signature];
delete globalQueue.queuedRequests[signature];
};
let requestCounter = 0;
export default class Rest extends Http {
constructor(resource, config) {
super(resource, config);
this.store = config.store;
this.logEndpoints = Boolean(config.logEndpoints);
this.logInstance = Boolean(config.logInstance);
this.vrrModuleName = config.vrrModuleName;
const {logEndpoints, logInstance, store} = this;
this.updateStore = function updateStore(storeAction, payload) {
if (logEndpoints || logInstance) {
store.dispatch(storeAction, payload);
}
};
}
// Dispatcher methods (overrides HTTP dispatch method)
dispatch(action, {endpoint, handler, callback, apiModel, apiModule, deletedId, callerInstance}, ...args) {
const mutation = [apiModule, `${action}${capitalizeFirst(apiModel)}`].filter(Boolean).join('/');
const actionType = action === 'list' ? 'get' : action; // axios has no 'list'
const REGISTER_REQUEST = `${this.vrrModuleName}/registerRequest`;
const UPDATE_REQUEST = `${this.vrrModuleName}/updateRequest`;
const DELETE_INSTANCE = `${this.vrrModuleName}/deleteInstance`;
const {logEndpoints, logInstance, updateStore} = this;
let instanceUUID = null;
if (logInstance) {
instanceUUID = componentRegisterMap.add(callerInstance);
if (callerInstance && callerInstance.$once) {
callerInstance.$once('hook:beforeDestroy', () => {
updateStore(DELETE_INSTANCE, instanceUUID);
});
}
}
let discard = false;
// prepare for request timeout
let timeout = false;
/*
* Status types:
* - registered (before axios is called)
* - success
* - failed
* - slow
* - timeout
* - pending
*/
const request = this.register(
actionType,
{apiModel, apiModule, endpoint, callerInstance: instanceUUID, logEndpoints, logInstance},
...args,
);
request.cancel = () => {
discard = true;
updateStore(UPDATE_REQUEST, {
...request,
status: 'canceled',
completed: Date.now(),
});
};
updateStore(REGISTER_REQUEST, {
...request,
});
// prepare for slow request
const slowRequest = setTimeout(() => {
updateStore(UPDATE_REQUEST, {
...request,
status: 'slow',
});
}, this.slowTimeout);
const requestTimeout = setTimeout(() => {
timeout = true;
updateStore(UPDATE_REQUEST, {
...request,
completed: Date.now(),
status: 'timeout',
});
}, this.failedTimeout);
const ajax = this.handleQueue(request, actionType, endpoint, ...args);
/* @todo: add a global warning component when requests fail */
// tell the store a request was fired
updateStore(UPDATE_REQUEST, {
...request,
status: 'pending',
});
ajax
.then((res) => {
clearTimeout(slowRequest);
clearTimeout(requestTimeout);
if (timeout || discard) {
return undefined;
}
const response = !res && action === 'delete' ? deletedId : res;
const responseCopy = JSON.parse(JSON.stringify({data: response.data}));
const data = handler(responseCopy, this.store);
/*
* About using callbacks here:
* Sometimes the data Axios gets needs to be processed. We can do this in
* the Store or in the Controller of the component. Use callback & Controller
* pattern if you want to keep the store "logic free".
*/
if (callback) {
// Used in some controllers when data from server needs to be processed before being set in store
callback(data, this.store);
} else {
updateStore(mutation, data);
}
const updated = {
...request,
completed: Date.now(),
response: data,
status: 'success',
};
updateStore(UPDATE_REQUEST, updated);
// lets use setTimeout so we don't remove the request before the Subscriber promise resolves
setTimeout(() => this.unregister(request), 1);
const signature = getRequestSignature(request);
const activeRequest = globalQueue.activeRequests[signature];
if (activeRequest && activeRequest.id === request.id) {
globalQueue.queuedRequests[signature].forEach((queued) => {
queued.request.status = updated.status;
queued.request.completed = updated.completed;
queued.request.Promise.resolve(response); // resolve pending requests with same response
setTimeout(() => this.unregister(queued.request), 1);
});
globalQueue.queuedRequests[signature] = []; // done, reset pending requests array
delete globalQueue.activeRequests[signature]; // done, remove the active request pointer
}
return undefined;
})
.catch((err) => {
clearTimeout(slowRequest);
clearTimeout(requestTimeout);
this.unregister(request);
const updated = {
...request,
completed: Date.now(),
response: err.response && err.response.data,
status: 'failed',
internalError: err,
};
updateStore(UPDATE_REQUEST, updated);
handleQueueOnBadRequest(request);
});
const {store} = this;
return new Promise((resolve, reject) => {
new Subscriber(endpoint, request.id, store, UPDATE_REQUEST).onSuccess(resolve).onFail((data) => {
handleQueueOnBadRequest(request);
reject(data);
});
});
}
handleQueue(request, action, endpoint, ...args) {
if (action !== 'get') {
// NB: check comment text about implementation of "update" requests inside queue of "get"s (on top of this file)
return axios[action](endpoint, ...args);
}
// check if there is a active request to the same endpoint
const signature = getRequestSignature(request);
const activeRequest = globalQueue.activeRequests[signature];
if (!activeRequest) {
globalQueue.activeRequests[signature] = request;
return axios[action](endpoint, ...args);
}
if (!globalQueue.queuedRequests[signature]) {
globalQueue.queuedRequests[signature] = [];
}
// pending request already registered, queue this request
globalQueue.queuedRequests[signature].push({
action,
args,
endpoint,
request,
});
const executor = function executor(resolve, reject) {
request.Promise = {reject, resolve};
};
const deferred = new Promise(executor);
request.Promise.instance = deferred;
return deferred;
}
register(action, moduleInfo, ...args) {
requestCounter += 1;
const id = [moduleInfo.apiModule, moduleInfo.apiModel, requestCounter].join('_');
const httpData = args.find((obj) => obj.params);
const params = httpData && httpData.params;
return {
...moduleInfo,
action,
created: Date.now(),
id,
params,
status: 'registered',
};
}
unregister(request) {
const UNREGISTER = `${this.vrrModuleName}/unregisterRequest`;
this.updateStore(UNREGISTER, request);
}
}