diff --git a/README.md b/README.md index 79d389d..23e932d 100644 --- a/README.md +++ b/README.md @@ -524,3 +524,10 @@ animalStore.fetch().then(() => { console.log(animalStore.at(0).name); // Garfield }); ``` + +You can also cancel the previous request by passing `{ cancelPreviousFetch: true }` to fetch + +```js +animalStore.fetch(); // request cancelled +animalStore.fetch({ cancelPreviousFetch: true }); +``` diff --git a/dist/mobx-spine.cjs.js b/dist/mobx-spine.cjs.js index 13ac9a7..b7015f6 100644 --- a/dist/mobx-spine.cjs.js +++ b/dist/mobx-spine.cjs.js @@ -234,11 +234,16 @@ function _applyDecoratedDescriptor(target, property, decorators, descriptor, con return desc; } + var AVAILABLE_CONST_OPTIONS = ['relations', 'limit', 'comparator', 'params', 'repository']; var Store = (_class = (_temp = _class2 = function () { createClass(Store, [{ key: 'url', + + // The set of models has changed + + // Holds the fetch parameters value: function url() { // Try to auto-generate the URL. var bname = this.constructor.backendResourceName; @@ -247,10 +252,6 @@ var Store = (_class = (_temp = _class2 = function () { } return null; } - // The set of models has changed - - // Holds the fetch parameters - }, { key: 'initialize', @@ -296,6 +297,7 @@ var Store = (_class = (_temp = _class2 = function () { lodash.forIn(options, function (value, option) { invariant(AVAILABLE_CONST_OPTIONS.includes(option), 'Unknown option passed to store: ' + option); }); + this.abortController = new AbortController(); this.__repository = options.repository; if (options.relations) { this.__parseRelations(options.relations); @@ -481,6 +483,12 @@ var Store = (_class = (_temp = _class2 = function () { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + if (options.cancelPreviousFetch) { + this.abortController.abort(); + this.abortController = new AbortController(); + this.__pendingRequestCount = 0; + } + options.abortSignal = this.abortController.signal; var data = this.buildFetchData(options); var promise = this.wrapPendingRequestCount(this.__getApi().fetchStore({ @@ -492,7 +500,15 @@ var Store = (_class = (_temp = _class2 = function () { _this5.fromBackend(res); return res.response; - }))); + })).catch(function (e) { + if (Axios.isCancel(e)) { + // correct __pendingRequestCount + _this5.__pendingRequestCount++; + return null; + } else { + throw e; + } + })); return promise; } @@ -2257,7 +2273,7 @@ function checkLuxonDateTime(attr, value) { } var LUXON_DATE_FORMAT = 'yyyy-LL-dd'; -var LUXON_DATETIME_FORMAT = "yyyy'-'LL'-'dd'T'HH':'mm':'ssZZ"; +var LUXON_DATETIME_FORMAT = 'yyyy\'-\'LL\'-\'dd\'T\'HH\':\'mm\':\'ssZZ'; var CASTS = { momentDate: { diff --git a/dist/mobx-spine.es.js b/dist/mobx-spine.es.js index 44436ae..c0441b5 100644 --- a/dist/mobx-spine.es.js +++ b/dist/mobx-spine.es.js @@ -228,11 +228,16 @@ function _applyDecoratedDescriptor(target, property, decorators, descriptor, con return desc; } + var AVAILABLE_CONST_OPTIONS = ['relations', 'limit', 'comparator', 'params', 'repository']; var Store = (_class = (_temp = _class2 = function () { createClass(Store, [{ key: 'url', + + // The set of models has changed + + // Holds the fetch parameters value: function url() { // Try to auto-generate the URL. var bname = this.constructor.backendResourceName; @@ -241,10 +246,6 @@ var Store = (_class = (_temp = _class2 = function () { } return null; } - // The set of models has changed - - // Holds the fetch parameters - }, { key: 'initialize', @@ -290,6 +291,7 @@ var Store = (_class = (_temp = _class2 = function () { forIn(options, function (value, option) { invariant(AVAILABLE_CONST_OPTIONS.includes(option), 'Unknown option passed to store: ' + option); }); + this.abortController = new AbortController(); this.__repository = options.repository; if (options.relations) { this.__parseRelations(options.relations); @@ -475,6 +477,12 @@ var Store = (_class = (_temp = _class2 = function () { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + if (options.cancelPreviousFetch) { + this.abortController.abort(); + this.abortController = new AbortController(); + this.__pendingRequestCount = 0; + } + options.abortSignal = this.abortController.signal; var data = this.buildFetchData(options); var promise = this.wrapPendingRequestCount(this.__getApi().fetchStore({ @@ -486,7 +494,15 @@ var Store = (_class = (_temp = _class2 = function () { _this5.fromBackend(res); return res.response; - }))); + })).catch(function (e) { + if (Axios.isCancel(e)) { + // correct __pendingRequestCount + _this5.__pendingRequestCount++; + return null; + } else { + throw e; + } + })); return promise; } @@ -2251,7 +2267,7 @@ function checkLuxonDateTime(attr, value) { } var LUXON_DATE_FORMAT = 'yyyy-LL-dd'; -var LUXON_DATETIME_FORMAT = "yyyy'-'LL'-'dd'T'HH':'mm':'ssZZ"; +var LUXON_DATETIME_FORMAT = 'yyyy\'-\'LL\'-\'dd\'T\'HH\':\'mm\':\'ssZZ'; var CASTS = { momentDate: { diff --git a/package.json b/package.json index b7231b3..ee6b24e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mobx-spine", - "version": "0.28.5", + "version": "0.28.6", "license": "ISC", "author": "Kees Kluskens ", "description": "MobX with support for models, relations and an API.", diff --git a/src/Store.js b/src/Store.js index 971e964..4c97973 100644 --- a/src/Store.js +++ b/src/Store.js @@ -12,6 +12,8 @@ import { uniqBy, } from 'lodash'; import { invariant } from './utils'; +import Axios from 'axios'; + const AVAILABLE_CONST_OPTIONS = [ 'relations', 'limit', @@ -37,6 +39,7 @@ export default class Store { __activeRelations = []; Model = null; api = null; + abortController; __repository; static backendResourceName = ''; @@ -80,6 +83,7 @@ export default class Store { `Unknown option passed to store: ${option}` ); }); + this.abortController = new AbortController(); this.__repository = options.repository; if (options.relations) { this.__parseRelations(options.relations); @@ -264,6 +268,12 @@ export default class Store { @action fetch(options = {}) { + if (options.cancelPreviousFetch) { + this.abortController.abort(); + this.abortController = new AbortController(); + this.__pendingRequestCount = 0; + } + options.abortSignal = this.abortController.signal; const data = this.buildFetchData(options); const promise = this.wrapPendingRequestCount( @@ -279,6 +289,15 @@ export default class Store { return res.response; })) + .catch(e => { + if (Axios.isCancel(e)) { + // correct __pendingRequestCount + this.__pendingRequestCount++ + return null; + } else { + throw e; + } + }) ); return promise; diff --git a/src/__tests__/Store.js b/src/__tests__/Store.js index 37e7eef..e6dabb0 100644 --- a/src/__tests__/Store.js +++ b/src/__tests__/Store.js @@ -24,6 +24,7 @@ import customersWithTownRestaurantsUnbalanced from './fixtures/customers-with-to import townsWithRestaurantsAndCustomersNoIdList from './fixtures/towns-with-restaurants-and-customers-no-id-list.json'; import customersWithOldTowns from './fixtures/customers-with-old-towns.json'; import animalsData from './fixtures/animals.json'; +import animalsDataIdOne from './fixtures/animals-id-1.json'; import pagination0Data from './fixtures/pagination/0.json'; import pagination1Data from './fixtures/pagination/1.json'; import pagination2Data from './fixtures/pagination/2.json'; @@ -740,6 +741,81 @@ describe('requests', () => { expect(animalStore.isLoading).toBe(false); }); }); + + test('fetch cancelPreviousFetch should update pendingRequestCount', async () => { + const animalStore = new AnimalStore(); + + mock.onAny().reply(config => { + return new Promise((resolve, reject) => { + if (config.signal.aborted) { + setTimeout(() => { + // reject after 1 second + reject({ __CANCEL__: true }) + }, 1000) + } + setTimeout(() => { + resolve([200, animalsDataIdOne]) + }, 100) + }) + }); + const p1 = animalStore.fetch() + const p2 = animalStore.fetch({ cancelPreviousFetch: true, params: { id: 1 } }) + + // we ignore previous request, so number of pending = 1 + expect(animalStore.__pendingRequestCount).toBe(1); + expect(animalStore.isLoading).toBe(true); + expect(animalStore.models.length).toBe(0); + + // p2 is first to resolve, should mark store as "done" + await p2 + expect(animalStore.__pendingRequestCount).toBe(0); + expect(animalStore.isLoading).toBe(false); + expect(animalStore.models.length).toBe(1); + + // p1 is cancelled, should not change stuff + await p1 + expect(animalStore.__pendingRequestCount).toBe(0); + expect(animalStore.isLoading).toBe(false); + expect(animalStore.models.length).toBe(1); + }); + + test('fetch cancelPreviousFetch when received in order, but canceled', async () => { + const animalStore = new AnimalStore(); + + mock.onAny().reply(config => { + return new Promise((resolve, reject) => { + if (config.signal.aborted) { + setTimeout(() => { + // reject after 100 ms + reject({ __CANCEL__: true }) + }, 100) + } + setTimeout(() => { + // resolve other request later + resolve([200, animalsDataIdOne]) + }, 1000) + }) + }); + const p1 = animalStore.fetch() + const p2 = animalStore.fetch({ cancelPreviousFetch: true, params: { id: 1 } }) + + // we ignore previous request, so number of pending = 1 + expect(animalStore.__pendingRequestCount).toBe(1); + expect(animalStore.isLoading).toBe(true); + expect(animalStore.models.length).toBe(0); + + // p1 is first to resolve, should be cancelled + await p1 + expect(animalStore.__pendingRequestCount).toBe(1); + expect(animalStore.isLoading).toBe(true); + expect(animalStore.models.length).toBe(0); + + // p2 is resolved, data should udpate + await p2 + expect(animalStore.__pendingRequestCount).toBe(0); + expect(animalStore.isLoading).toBe(false); + expect(animalStore.models.length).toBe(1); + }); }); describe('Pagination', () => { diff --git a/src/__tests__/fixtures/animals-id-1.json b/src/__tests__/fixtures/animals-id-1.json new file mode 100644 index 0000000..a087a46 --- /dev/null +++ b/src/__tests__/fixtures/animals-id-1.json @@ -0,0 +1,11 @@ +{ + "data": [ + { + "id": 1, + "name": "Madagascar" + } + ], + "meta": { + "total_records": 1 + } +}