Skip to content

Commit

Permalink
feat: Add local caching
Browse files Browse the repository at this point in the history
  • Loading branch information
iamareebjamal committed Nov 18, 2020
1 parent 91a069b commit 6222886
Show file tree
Hide file tree
Showing 11 changed files with 181 additions and 72 deletions.
23 changes: 11 additions & 12 deletions app/components/footer-main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@ import classic from 'ember-classic-decorator';
import { classNames, tagName } from '@ember-decorators/component';
import { action, computed } from '@ember/object';
import Component from '@ember/component';
import { filterBy } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import { sortBy } from 'lodash-es';

@classic
@tagName('footer')
@classNames('ui', 'inverted', 'vertical', 'footer', 'segment')
export default class FooterMain extends Component {

@service cache;

@filterBy('pages', 'place', 'footer')
footerPages;

@computed
get currentLocale() {
return this.l10n.getLocale();
Expand All @@ -17,17 +26,7 @@ export default class FooterMain extends Component {
this.l10n.switchLanguage(locale);
}

didInsertElement() {
this.set('eventLocations', this.eventLocations.sortBy('name'));

const eventTypes = this.eventTypes.sortBy('name').toArray();
eventTypes.forEach(eventType => {
if (eventType.name === 'Other') {
const other = eventType;
eventTypes.splice(eventTypes.indexOf(eventType), 1);
eventTypes.push(other);
}
});
this.set('eventTypes', eventTypes);
async didInsertElement() {
this.set('pages', sortBy(await this.cache.findAll('page'), 'index'));
}
}
3 changes: 0 additions & 3 deletions app/controllers/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ export default class ApplicationController extends Controller {
@filterBy('model.notifications', 'isRead', false)
unreadNotifications;

@filterBy('model.pages', 'place', 'footer')
footerPages;

getCookieSeen(write) {
const cookieName = 'seen-cookie-message';
const cookie = this.cookies.read(cookieName);
Expand Down
12 changes: 6 additions & 6 deletions app/controllers/events/view/tickets/orders/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ export default class extends Controller.extend(EmberTableControllerMixin) {
}
},
{
name : 'First Name',
valuePath : 'user.firstName',
width : 50
name : 'First Name',
valuePath : 'user.firstName',
width : 50
},
{
name : 'Last Name',
valuePath : 'user.lastName',
width : 50
name : 'Last Name',
valuePath : 'user.lastName',
width : 50
},
{
name : 'Date and Time',
Expand Down
35 changes: 12 additions & 23 deletions app/routes/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin';
import { merge, values, isEmpty } from 'lodash-es';
import { hash } from 'rsvp';

@classic
export default class ApplicationRoute extends Route.extend(ApplicationRouteMixin) {
Expand All @@ -13,6 +14,9 @@ export default class ApplicationRoute extends Route.extend(ApplicationRouteMixin
@service
currentUser;

@service
cache;

title(tokens) {
if (!tokens) {
tokens = [];
Expand All @@ -36,10 +40,10 @@ export default class ApplicationRoute extends Route.extend(ApplicationRouteMixin
}

async model() {
let notificationsPromise = Promise.resolve([]);
let notifications = Promise.resolve([]);
if (this.session.isAuthenticated) {
try {
notificationsPromise = this.authManager.currentUser.query('notifications', {
notifications = this.authManager.currentUser.query('notifications', {
filter: [
{
name : 'is-read',
Expand All @@ -55,30 +59,15 @@ export default class ApplicationRoute extends Route.extend(ApplicationRouteMixin
}
}

const pagesPromise = this.store.query('page', {
sort: 'index'
});

const settingsPromise = this.store.queryRecord('setting', {});
const eventTypesPromise = this.store.findAll('event-type');
const eventLocationsPromise = this.store.findAll('event-location');

const [notifications, pages, settings, eventTypes, eventLocations] = await Promise.all([
notificationsPromise,
pagesPromise,
settingsPromise,
eventTypesPromise,
eventLocationsPromise]);
const pages = this.cache.findAll('page');

return {
return hash({
notifications,
pages,
cookiePolicy : settings.cookiePolicy,
cookiePolicyLink : settings.cookiePolicyLink,
socialLinks : settings,
eventTypes,
eventLocations
};
cookiePolicy : this.settings.cookiePolicy,
cookiePolicyLink : this.settings.cookiePolicyLink,
socialLinks : this.settings
});
}

sessionInvalidated() {
Expand Down
4 changes: 4 additions & 0 deletions app/services/auth-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export default class AuthManagerService extends Service {
@service
bugTracker;

@service
cache;

@computed('session.data.currentUserFallback.id', 'currentUserModel')
get currentUser() {
if (this.currentUserModel) {
Expand Down Expand Up @@ -64,6 +67,7 @@ export default class AuthManagerService extends Service {
this.session.invalidate();
this.set('currentUserModel', null);
this.session.set('data.currentUserFallback', null);
this.cache.clear();
}

identify() {
Expand Down
130 changes: 130 additions & 0 deletions app/services/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/* eslint-disable no-console, prefer-rest-params */
import Service, { inject as service } from '@ember/service';
import DS from 'ember-data';

function pushToStore(store: DS.Store, data: any): any[] | any {
const parsed = data?.value;
if (Array.isArray(parsed)) {
const items = []
for (const item of parsed) {
store.pushPayload(item);
items.push(store.peekRecord(item.data.type, item.data.id));
}
return items;
} else {
store.pushPayload(parsed);

return store.peekRecord(parsed.data.type, parsed.data.id);
}
}

function saveToStorage(key: string, value: any | null) {
if (!value) {return}
let serialized = null;
if (Array.isArray(value.content)) {
serialized = value.map((v: any) => v.serialize({ includeId: true }));
} else {
serialized = value.serialize({ includeId: true });
}

localStorage.setItem(key, JSON.stringify({
time : Date.now(),
value : serialized
}));
}

export default class Cache extends Service.extend({
// anything which *must* be merged to prototype here
}) {
version = 'v1';

@service store!: DS.Store;

get prefix(): string {
return 'cache:' + this.version + ':';
}

isExpired(data: { time: number, value: any} | null): boolean {
// Item expired after 15 seconds
return Boolean(data?.time && (Date.now() - data?.time) > 60 * 1000)
}

async passThrough(key: string, callable: () => any): Promise<any> {
const value = await callable();
saveToStorage(key, value);

return value;
}

async cacheData(key: string, callable: () => any): Promise<any | null> {
key = this.prefix + key;
const stored = localStorage.getItem(key);
try {
if (stored) {
const data = JSON.parse(stored);

if (!data.time) {
// Invalid data structure
return this.passThrough(key, callable);
}

const expired = this.isExpired(data);
const item = pushToStore(this.store, data);

if (expired) {
// Revalidate resource while serving stale
console.info('Item expired. Revalidating...', key);
this.passThrough(key, callable);
}

return item;
} else {
return this.passThrough(key, callable);
}
} catch (e) {
console.error('Error while loading value from cache using key: ' + key, e);

return callable();
}
}

async findAll(model: string, options: any | null): Promise<any> {
const saved = await this.cacheData(model, () => this.store.findAll(model, options));
if (saved) {return saved;}
return this.store.peekAll(model);
}

async queryRecord(key: string, model: string, options: any | null): Promise<any> {
const saved = await this.cacheData(key, () => this.store.queryRecord(model, options));
if (saved) {return saved;}
return this.store.peekRecord(model, 1);
}

clear(): void {
for (const key of Object.keys(localStorage)) {
if (key.startsWith(this.prefix)) {
console.info('Clearing cache entry:', key);
localStorage.removeItem(key);
}
}
}

constructor() {
super(...arguments);
for (const key of Object.keys(localStorage)) {
if (key.startsWith('cache:')) {
if (!key.startsWith(this.prefix)) {
console.info('Removing previous cache entry:', key);
localStorage.removeItem(key);
}
}
}
}
}

// DO NOT DELETE: this is how TypeScript knows how to look up your services.
declare module '@ember/service' {
interface Registry {
'cache': Cache;
}
}
3 changes: 2 additions & 1 deletion app/services/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { observer } from '@ember/object';

export default Service.extend({

cache : service(),
store : service(),
session : service(),
authManager : service(),
Expand All @@ -25,7 +26,7 @@ export default Service.extend({
* @private
*/
async _loadSettings() {
const settingsModel = await this.store.queryRecord('setting', {});
const settingsModel = await this.cache.queryRecord('settings', 'setting', {});
this.store.modelFor('setting').eachAttribute(attributeName => {
this.set(attributeName, settingsModel.get(attributeName));
});
Expand Down
2 changes: 1 addition & 1 deletion app/templates/application.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
</div>


<FooterMain @eventTypes={{this.model.eventTypes}} @eventLocations={{this.model.eventLocations}} @socialLinks={{this.model.socialLinks}} @footerPages={{this.footerPages}} />
<FooterMain @socialLinks={{this.model.socialLinks}} />

</div>
</SideBar>
Expand Down
2 changes: 1 addition & 1 deletion app/utils/dictionary/demography.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1003,4 +1003,4 @@ export const countries = [
name : 'Zimbabwe',
code : 'ZW'
}
];
];
26 changes: 1 addition & 25 deletions tests/integration/components/footer-main-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,9 @@ import { render } from '@ember/test-helpers';
module('Integration | Component | footer main', function(hooks) {
setupIntegrationTest(hooks);

const eventLocations = [
{
name : 'Berlin',
slug : 'berlin'
},
{
name : 'New Delhi',
slug : 'new-delhi'
}
];

const eventTypes = [
{
name : 'Conference',
slug : 'conference'
},
{
name : 'Meetup',
slug : 'meetup'
}
];


test('it renders', async function(assert) {
this.set('eventTypes', eventTypes);
this.set('eventLocations', eventLocations);
await render(hbs`{{footer-main l10n=l10n eventLocations=eventLocations eventTypes=eventTypes}}`);
await render(hbs`{{footer-main l10n=l10n}}`);
assert.ok(this.element.innerHTML.trim().includes('footer'));
});
});
13 changes: 13 additions & 0 deletions tests/unit/services/cache-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';

module('Unit | Service | cache', function(hooks) {
setupTest(hooks);

// Replace this with your real tests.
test('it exists', function(assert) {
const service = this.owner.lookup('service:cache');
assert.ok(service);
});
});

0 comments on commit 6222886

Please sign in to comment.