Skip to content

Commit 6222886

Browse files
committed
feat: Add local caching
1 parent 91a069b commit 6222886

File tree

11 files changed

+181
-72
lines changed

11 files changed

+181
-72
lines changed

app/components/footer-main.js

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,20 @@ import classic from 'ember-classic-decorator';
22
import { classNames, tagName } from '@ember-decorators/component';
33
import { action, computed } from '@ember/object';
44
import Component from '@ember/component';
5+
import { filterBy } from '@ember/object/computed';
6+
import { inject as service } from '@ember/service';
7+
import { sortBy } from 'lodash-es';
58

69
@classic
710
@tagName('footer')
811
@classNames('ui', 'inverted', 'vertical', 'footer', 'segment')
912
export default class FooterMain extends Component {
13+
14+
@service cache;
15+
16+
@filterBy('pages', 'place', 'footer')
17+
footerPages;
18+
1019
@computed
1120
get currentLocale() {
1221
return this.l10n.getLocale();
@@ -17,17 +26,7 @@ export default class FooterMain extends Component {
1726
this.l10n.switchLanguage(locale);
1827
}
1928

20-
didInsertElement() {
21-
this.set('eventLocations', this.eventLocations.sortBy('name'));
22-
23-
const eventTypes = this.eventTypes.sortBy('name').toArray();
24-
eventTypes.forEach(eventType => {
25-
if (eventType.name === 'Other') {
26-
const other = eventType;
27-
eventTypes.splice(eventTypes.indexOf(eventType), 1);
28-
eventTypes.push(other);
29-
}
30-
});
31-
this.set('eventTypes', eventTypes);
29+
async didInsertElement() {
30+
this.set('pages', sortBy(await this.cache.findAll('page'), 'index'));
3231
}
3332
}

app/controllers/application.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,6 @@ export default class ApplicationController extends Controller {
2020
@filterBy('model.notifications', 'isRead', false)
2121
unreadNotifications;
2222

23-
@filterBy('model.pages', 'place', 'footer')
24-
footerPages;
25-
2623
getCookieSeen(write) {
2724
const cookieName = 'seen-cookie-message';
2825
const cookie = this.cookies.read(cookieName);

app/controllers/events/view/tickets/orders/list.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ export default class extends Controller.extend(EmberTableControllerMixin) {
2020
}
2121
},
2222
{
23-
name : 'First Name',
24-
valuePath : 'user.firstName',
25-
width : 50
23+
name : 'First Name',
24+
valuePath : 'user.firstName',
25+
width : 50
2626
},
2727
{
28-
name : 'Last Name',
29-
valuePath : 'user.lastName',
30-
width : 50
28+
name : 'Last Name',
29+
valuePath : 'user.lastName',
30+
width : 50
3131
},
3232
{
3333
name : 'Date and Time',

app/routes/application.js

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { inject as service } from '@ember/service';
44
import Route from '@ember/routing/route';
55
import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin';
66
import { merge, values, isEmpty } from 'lodash-es';
7+
import { hash } from 'rsvp';
78

89
@classic
910
export default class ApplicationRoute extends Route.extend(ApplicationRouteMixin) {
@@ -13,6 +14,9 @@ export default class ApplicationRoute extends Route.extend(ApplicationRouteMixin
1314
@service
1415
currentUser;
1516

17+
@service
18+
cache;
19+
1620
title(tokens) {
1721
if (!tokens) {
1822
tokens = [];
@@ -36,10 +40,10 @@ export default class ApplicationRoute extends Route.extend(ApplicationRouteMixin
3640
}
3741

3842
async model() {
39-
let notificationsPromise = Promise.resolve([]);
43+
let notifications = Promise.resolve([]);
4044
if (this.session.isAuthenticated) {
4145
try {
42-
notificationsPromise = this.authManager.currentUser.query('notifications', {
46+
notifications = this.authManager.currentUser.query('notifications', {
4347
filter: [
4448
{
4549
name : 'is-read',
@@ -55,30 +59,15 @@ export default class ApplicationRoute extends Route.extend(ApplicationRouteMixin
5559
}
5660
}
5761

58-
const pagesPromise = this.store.query('page', {
59-
sort: 'index'
60-
});
61-
62-
const settingsPromise = this.store.queryRecord('setting', {});
63-
const eventTypesPromise = this.store.findAll('event-type');
64-
const eventLocationsPromise = this.store.findAll('event-location');
65-
66-
const [notifications, pages, settings, eventTypes, eventLocations] = await Promise.all([
67-
notificationsPromise,
68-
pagesPromise,
69-
settingsPromise,
70-
eventTypesPromise,
71-
eventLocationsPromise]);
62+
const pages = this.cache.findAll('page');
7263

73-
return {
64+
return hash({
7465
notifications,
7566
pages,
76-
cookiePolicy : settings.cookiePolicy,
77-
cookiePolicyLink : settings.cookiePolicyLink,
78-
socialLinks : settings,
79-
eventTypes,
80-
eventLocations
81-
};
67+
cookiePolicy : this.settings.cookiePolicy,
68+
cookiePolicyLink : this.settings.cookiePolicyLink,
69+
socialLinks : this.settings
70+
});
8271
}
8372

8473
sessionInvalidated() {

app/services/auth-manager.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ export default class AuthManagerService extends Service {
1919
@service
2020
bugTracker;
2121

22+
@service
23+
cache;
24+
2225
@computed('session.data.currentUserFallback.id', 'currentUserModel')
2326
get currentUser() {
2427
if (this.currentUserModel) {
@@ -64,6 +67,7 @@ export default class AuthManagerService extends Service {
6467
this.session.invalidate();
6568
this.set('currentUserModel', null);
6669
this.session.set('data.currentUserFallback', null);
70+
this.cache.clear();
6771
}
6872

6973
identify() {

app/services/cache.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/* eslint-disable no-console, prefer-rest-params */
2+
import Service, { inject as service } from '@ember/service';
3+
import DS from 'ember-data';
4+
5+
function pushToStore(store: DS.Store, data: any): any[] | any {
6+
const parsed = data?.value;
7+
if (Array.isArray(parsed)) {
8+
const items = []
9+
for (const item of parsed) {
10+
store.pushPayload(item);
11+
items.push(store.peekRecord(item.data.type, item.data.id));
12+
}
13+
return items;
14+
} else {
15+
store.pushPayload(parsed);
16+
17+
return store.peekRecord(parsed.data.type, parsed.data.id);
18+
}
19+
}
20+
21+
function saveToStorage(key: string, value: any | null) {
22+
if (!value) {return}
23+
let serialized = null;
24+
if (Array.isArray(value.content)) {
25+
serialized = value.map((v: any) => v.serialize({ includeId: true }));
26+
} else {
27+
serialized = value.serialize({ includeId: true });
28+
}
29+
30+
localStorage.setItem(key, JSON.stringify({
31+
time : Date.now(),
32+
value : serialized
33+
}));
34+
}
35+
36+
export default class Cache extends Service.extend({
37+
// anything which *must* be merged to prototype here
38+
}) {
39+
version = 'v1';
40+
41+
@service store!: DS.Store;
42+
43+
get prefix(): string {
44+
return 'cache:' + this.version + ':';
45+
}
46+
47+
isExpired(data: { time: number, value: any} | null): boolean {
48+
// Item expired after 15 seconds
49+
return Boolean(data?.time && (Date.now() - data?.time) > 60 * 1000)
50+
}
51+
52+
async passThrough(key: string, callable: () => any): Promise<any> {
53+
const value = await callable();
54+
saveToStorage(key, value);
55+
56+
return value;
57+
}
58+
59+
async cacheData(key: string, callable: () => any): Promise<any | null> {
60+
key = this.prefix + key;
61+
const stored = localStorage.getItem(key);
62+
try {
63+
if (stored) {
64+
const data = JSON.parse(stored);
65+
66+
if (!data.time) {
67+
// Invalid data structure
68+
return this.passThrough(key, callable);
69+
}
70+
71+
const expired = this.isExpired(data);
72+
const item = pushToStore(this.store, data);
73+
74+
if (expired) {
75+
// Revalidate resource while serving stale
76+
console.info('Item expired. Revalidating...', key);
77+
this.passThrough(key, callable);
78+
}
79+
80+
return item;
81+
} else {
82+
return this.passThrough(key, callable);
83+
}
84+
} catch (e) {
85+
console.error('Error while loading value from cache using key: ' + key, e);
86+
87+
return callable();
88+
}
89+
}
90+
91+
async findAll(model: string, options: any | null): Promise<any> {
92+
const saved = await this.cacheData(model, () => this.store.findAll(model, options));
93+
if (saved) {return saved;}
94+
return this.store.peekAll(model);
95+
}
96+
97+
async queryRecord(key: string, model: string, options: any | null): Promise<any> {
98+
const saved = await this.cacheData(key, () => this.store.queryRecord(model, options));
99+
if (saved) {return saved;}
100+
return this.store.peekRecord(model, 1);
101+
}
102+
103+
clear(): void {
104+
for (const key of Object.keys(localStorage)) {
105+
if (key.startsWith(this.prefix)) {
106+
console.info('Clearing cache entry:', key);
107+
localStorage.removeItem(key);
108+
}
109+
}
110+
}
111+
112+
constructor() {
113+
super(...arguments);
114+
for (const key of Object.keys(localStorage)) {
115+
if (key.startsWith('cache:')) {
116+
if (!key.startsWith(this.prefix)) {
117+
console.info('Removing previous cache entry:', key);
118+
localStorage.removeItem(key);
119+
}
120+
}
121+
}
122+
}
123+
}
124+
125+
// DO NOT DELETE: this is how TypeScript knows how to look up your services.
126+
declare module '@ember/service' {
127+
interface Registry {
128+
'cache': Cache;
129+
}
130+
}

app/services/settings.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { observer } from '@ember/object';
33

44
export default Service.extend({
55

6+
cache : service(),
67
store : service(),
78
session : service(),
89
authManager : service(),
@@ -25,7 +26,7 @@ export default Service.extend({
2526
* @private
2627
*/
2728
async _loadSettings() {
28-
const settingsModel = await this.store.queryRecord('setting', {});
29+
const settingsModel = await this.cache.queryRecord('settings', 'setting', {});
2930
this.store.modelFor('setting').eachAttribute(attributeName => {
3031
this.set(attributeName, settingsModel.get(attributeName));
3132
});

app/templates/application.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
</div>
2222

2323

24-
<FooterMain @eventTypes={{this.model.eventTypes}} @eventLocations={{this.model.eventLocations}} @socialLinks={{this.model.socialLinks}} @footerPages={{this.footerPages}} />
24+
<FooterMain @socialLinks={{this.model.socialLinks}} />
2525

2626
</div>
2727
</SideBar>

app/utils/dictionary/demography.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1003,4 +1003,4 @@ export const countries = [
10031003
name : 'Zimbabwe',
10041004
code : 'ZW'
10051005
}
1006-
];
1006+
];

tests/integration/components/footer-main-test.js

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,9 @@ import { render } from '@ember/test-helpers';
66
module('Integration | Component | footer main', function(hooks) {
77
setupIntegrationTest(hooks);
88

9-
const eventLocations = [
10-
{
11-
name : 'Berlin',
12-
slug : 'berlin'
13-
},
14-
{
15-
name : 'New Delhi',
16-
slug : 'new-delhi'
17-
}
18-
];
19-
20-
const eventTypes = [
21-
{
22-
name : 'Conference',
23-
slug : 'conference'
24-
},
25-
{
26-
name : 'Meetup',
27-
slug : 'meetup'
28-
}
29-
];
30-
319

3210
test('it renders', async function(assert) {
33-
this.set('eventTypes', eventTypes);
34-
this.set('eventLocations', eventLocations);
35-
await render(hbs`{{footer-main l10n=l10n eventLocations=eventLocations eventTypes=eventTypes}}`);
11+
await render(hbs`{{footer-main l10n=l10n}}`);
3612
assert.ok(this.element.innerHTML.trim().includes('footer'));
3713
});
3814
});

0 commit comments

Comments
 (0)