diff --git a/src/mixins/DataStore/DataStore.js b/src/mixins/DataStore/DataStore.js index 19f04d68..58b11d86 100644 --- a/src/mixins/DataStore/DataStore.js +++ b/src/mixins/DataStore/DataStore.js @@ -181,12 +181,13 @@ export const DataStore = ( fetch: flow(function* ({ id, query, pageNumber = null, reload = false, interaction, pageSize } = {}) { let currentViewId, currentViewQuery; const requestId = self.requestId = guidGenerator(); + const root = getRoot(self); if (id) { currentViewId = id; currentViewQuery = query; } else { - const currentView = getRoot(self).viewsStore.selected; + const currentView = root.viewsStore.selected; currentViewId = currentView?.id; currentViewQuery = currentView?.virtual ? currentView?.query : null; @@ -226,18 +227,18 @@ export const DataStore = ( if (interaction) Object.assign(params, { interaction }); - const data = yield getRoot(self).apiCall(apiMethod, params); + const data = yield root.apiCall(apiMethod, params, {}, { allowToCancel: root.SDK.type === 'DE' }); // We cancel current request processing if request id - // cnhaged during the request. It indicates that something + // changed during the request. It indicates that something // triggered another request while current one is not yet finished - if (requestId !== self.requestId) { + if (requestId !== self.requestId || data.isCanceled) { console.log(`Request ${requestId} was cancelled by another request`); return; } const highlightedID = self.highlighted; - const apiMethodSettings = getRoot(self).API.getSettingsByMethodName(apiMethod); + const apiMethodSettings = root.API.getSettingsByMethodName(apiMethod); const { total, [apiMethod]: list } = data; let associatedList = []; @@ -260,7 +261,7 @@ export const DataStore = ( self.loading = false; - getRoot(self).SDK.invoke('dataFetched', self); + root.SDK.invoke('dataFetched', self); }), reload: flow(function* ({ id, query, interaction } = {}) { diff --git a/src/stores/AppStore.js b/src/stores/AppStore.js index c0b597dd..11981664 100644 --- a/src/stores/AppStore.js +++ b/src/stores/AppStore.js @@ -135,6 +135,7 @@ export const AppStore = types .volatile(() => ({ needsDataFetch: false, projectFetch: false, + requestsInFlight: new Map(), })) .actions((self) => ({ startPolling() { @@ -541,16 +542,34 @@ export const AppStore = types * @param {string} methodName one of the methods in api-config * @param {object} params url vars and query string params * @param {object} body for POST/PATCH requests - * @param {{ errorHandler?: fn }} [options] additional options like errorHandler + * @param {{ errorHandler?: fn, headers?: object, allowToCancel?: boolean }} [options] additional options like errorHandler */ apiCall: flow(function* (methodName, params, body, options) { + const isAllowCancel = options?.allowToCancel; + const controller = new AbortController(); + const signal = controller.signal; const apiTransform = self.SDK.apiTransform?.[methodName]; const requestParams = apiTransform?.params?.(params) ?? params ?? {}; - const requestBody = apiTransform?.body?.(body) ?? body ?? undefined; - - let result = yield self.API[methodName](requestParams, requestBody); + const requestBody = apiTransform?.body?.(body) ?? body ?? {}; + const requestHeaders = apiTransform?.headers?.(options?.headers) ?? options?.headers ?? {}; + const requestKey = `${methodName}_${JSON.stringify(params || {})}`; + + if (isAllowCancel) { + requestHeaders.signal = signal; + if (self.requestsInFlight.has(requestKey)) { + /* if already in flight cancel the first in favor of new one */ + self.requestsInFlight.get(requestKey).abort(); + console.log(`Request ${requestKey} canceled`); + } + self.requestsInFlight.set(requestKey, controller); + } + let result = yield self.API[methodName](requestParams, { headers: requestHeaders, body: requestBody.body ?? requestBody }); - if (result.error && result.status !== 404) { + if (isAllowCancel) { + result.isCanceled = signal.aborted; + self.requestsInFlight.delete(requestKey); + } + if (result.error && result.status !== 404 && !signal.aborted) { if (options?.errorHandler?.(result)) { return result; } diff --git a/src/stores/Tabs/store.js b/src/stores/Tabs/store.js index 853dd66d..783fcfe6 100644 --- a/src/stores/Tabs/store.js +++ b/src/stores/Tabs/store.js @@ -302,7 +302,11 @@ export const TabStore = types const apiMethod = !view.saved && root.apiVersion === 2 ? "createTab" : "updateTab"; - const result = yield root.apiCall(apiMethod, params, body); + const result = yield root.apiCall(apiMethod, params, body, { allowToCancel: root.SDK.type === 'DE' }); + + if (result.isCanceled) { + return view; + } const viewSnapshot = getSnapshot(view); const newViewSnapshot = { ...viewSnapshot, diff --git a/src/stores/Tabs/tab.js b/src/stores/Tabs/tab.js index 204f61ee..2b29f45f 100644 --- a/src/stores/Tabs/tab.js +++ b/src/stores/Tabs/tab.js @@ -396,11 +396,11 @@ export const Tab = types } if (self.virtual) { yield self.dataStore.reload({ query: self.query, interaction }); - } else if (isFF(FF_LOPS_12) && self.root.SDK.type === 'labelops') { + } else if (isFF(FF_LOPS_12) && self.root.SDK?.type === 'labelops') { yield self.dataStore.reload({ query: self.query, interaction }); } - getRoot(self).SDK.invoke("tabReloaded", self); + getRoot(self).SDK?.invoke?.("tabReloaded", self); }), deleteFilter(filter) { @@ -439,7 +439,7 @@ export const Tab = types History.navigate({ tab: self.key }, true); self.reload({ interaction }); - } else if (isFF(FF_LOPS_12) && self.root.SDK.type === 'labelops') { + } else if (isFF(FF_LOPS_12) && self.root.SDK?.type === 'labelops') { const snapshot = self.serialize(); self.key = self.parent.snapshotToUrl(snapshot); diff --git a/src/utils/api-proxy/index.js b/src/utils/api-proxy/index.js index ff90eea2..653b720a 100644 --- a/src/utils/api-proxy/index.js +++ b/src/utils/api-proxy/index.js @@ -248,7 +248,7 @@ export class APIProxy { rawResponse = await fetch(apiCallURL, requestParams); } - if (raw) return rawResponse; + if (raw || rawResponse.isCanceled) return rawResponse; responseMeta = { headers: new Map(Array.from(rawResponse.headers)),