Memoized member stats with expiration

no issue

- added a `member-stats` service to keep member stats state outside of the chart component's lifecycle
- returns memoized member stats when fetching if the query hasn't changed and the data is less than a minute old
- reduces potentially heavy network requests when quickly navigating between members list and other screens
This commit is contained in:
Kevin Ansfield 2020-05-26 17:17:35 +01:00
parent b7b6c4b1b7
commit 86702ed949
5 changed files with 49 additions and 3 deletions

View File

@ -7,7 +7,7 @@ import {task} from 'ember-concurrency';
export default Component.extend({
ajax: service(),
ghostPaths: service(),
membersStats: service(),
// public attrs
nightShift: false,
@ -63,8 +63,7 @@ export default Component.extend({
// Tasks -------------------------------------------------------------------
fetchStatsTask: task(function* () {
let statsUrl = this.ghostPaths.url.api('members/stats');
let stats = yield this.ajax.request(statsUrl, {data: {days: this.selectedRange.days}});
let stats = yield this.membersStats.fetch({days: this.selectedRange.days});
this.set('stats', stats);

View File

@ -1,8 +1,11 @@
import ModalComponent from 'ghost-admin/components/modal-base';
import {alias} from '@ember/object/computed';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
export default ModalComponent.extend({
membersStats: service(),
// Allowed actions
confirm: () => {},
@ -17,6 +20,7 @@ export default ModalComponent.extend({
deleteMember: task(function* () {
try {
yield this.confirm();
this.membersStats.invalidate();
} finally {
this.send('closeModal');
}

View File

@ -1,8 +1,11 @@
import ModalComponent from 'ghost-admin/components/modal-base';
import ghostPaths from 'ghost-admin/utils/ghost-paths';
import {computed} from '@ember/object';
import {inject as service} from '@ember/service';
export default ModalComponent.extend({
membersStats: service(),
labelText: 'Select or drag-and-drop a CSV File',
response: null,
@ -26,6 +29,7 @@ export default ModalComponent.extend({
uploadSuccess(response) {
this.set('response', response.meta.stats);
this.membersStats.invalidate();
// invoke the passed in confirm action
this.confirm();
},

View File

@ -13,6 +13,7 @@ export default class MemberController extends Controller {
@controller members;
@service session;
@service dropdown;
@service membersStats;
@service notifications;
@service router;
@service store;
@ -66,6 +67,7 @@ export default class MemberController extends Controller {
@action
deleteMember() {
return this.member.destroyRecord().then(() => {
this.membersStats.invalidate();
return this.transitionToRoute('members');
}, (error) => {
return this.notifications.showAPIError(error, {key: 'member.delete'});
@ -114,6 +116,10 @@ export default class MemberController extends Controller {
let scratchProps = scratchMember.getProperties(SCRATCH_PROPS);
member.setProperties(scratchProps);
if (!member.isNew) {
this.membersStats.invalidate();
}
try {
yield member.save();
member.updateLabels();

View File

@ -0,0 +1,33 @@
import Service from '@ember/service';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
export default class MembersStatsService extends Service {
@service ajax;
@service ghostPaths;
@tracked stats = null;
fetch({days}) {
// return existing stats unless data is > 1 min old
let daysChanged = days === this._days;
let staleData = this._lastFetched - new Date() > 1 * 60 * 1000;
if (!this._forceRefresh && !daysChanged && !staleData) {
return Promise.resolve(this.stats);
}
this._days = days;
this._lastFetched = new Date();
let statsUrl = this.ghostPaths.url.api('members/stats');
return this.ajax.request(statsUrl, {data: {days}}).then((stats) => {
this.stats = stats;
return stats;
});
}
invalidate() {
this._forceRefresh = true;
}
}