From 6222886309ace61e00fe7399251c525f74f4c64c Mon Sep 17 00:00:00 2001 From: iamareebjamal Date: Thu, 19 Nov 2020 01:37:36 +0530 Subject: [PATCH] feat: Add local caching --- app/components/footer-main.js | 23 ++-- app/controllers/application.js | 3 - .../events/view/tickets/orders/list.js | 12 +- app/routes/application.js | 35 ++--- app/services/auth-manager.js | 4 + app/services/cache.ts | 130 ++++++++++++++++++ app/services/settings.js | 3 +- app/templates/application.hbs | 2 +- app/utils/dictionary/demography.ts | 2 +- .../components/footer-main-test.js | 26 +--- tests/unit/services/cache-test.ts | 13 ++ 11 files changed, 181 insertions(+), 72 deletions(-) create mode 100644 app/services/cache.ts create mode 100644 tests/unit/services/cache-test.ts diff --git a/app/components/footer-main.js b/app/components/footer-main.js index 8640fe6e887..bde6dd4318f 100644 --- a/app/components/footer-main.js +++ b/app/components/footer-main.js @@ -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(); @@ -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')); } } diff --git a/app/controllers/application.js b/app/controllers/application.js index 1f4d38dd6fb..f91a71507fe 100644 --- a/app/controllers/application.js +++ b/app/controllers/application.js @@ -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); diff --git a/app/controllers/events/view/tickets/orders/list.js b/app/controllers/events/view/tickets/orders/list.js index 36c2b206a8e..48f3968b0dc 100644 --- a/app/controllers/events/view/tickets/orders/list.js +++ b/app/controllers/events/view/tickets/orders/list.js @@ -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', diff --git a/app/routes/application.js b/app/routes/application.js index 8a105d83f52..f8f0fdd1c03 100644 --- a/app/routes/application.js +++ b/app/routes/application.js @@ -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) { @@ -13,6 +14,9 @@ export default class ApplicationRoute extends Route.extend(ApplicationRouteMixin @service currentUser; + @service + cache; + title(tokens) { if (!tokens) { tokens = []; @@ -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', @@ -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() { diff --git a/app/services/auth-manager.js b/app/services/auth-manager.js index d82159b23b9..fe089065eb3 100644 --- a/app/services/auth-manager.js +++ b/app/services/auth-manager.js @@ -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) { @@ -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() { diff --git a/app/services/cache.ts b/app/services/cache.ts new file mode 100644 index 00000000000..42b484fda02 --- /dev/null +++ b/app/services/cache.ts @@ -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 { + const value = await callable(); + saveToStorage(key, value); + + return value; + } + + async cacheData(key: string, callable: () => any): Promise { + 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 { + 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 { + 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; + } +} diff --git a/app/services/settings.js b/app/services/settings.js index 9e143618378..84f9822b12d 100644 --- a/app/services/settings.js +++ b/app/services/settings.js @@ -3,6 +3,7 @@ import { observer } from '@ember/object'; export default Service.extend({ + cache : service(), store : service(), session : service(), authManager : service(), @@ -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)); }); diff --git a/app/templates/application.hbs b/app/templates/application.hbs index cdb7e5e1766..d5044d5943f 100644 --- a/app/templates/application.hbs +++ b/app/templates/application.hbs @@ -21,7 +21,7 @@ - + diff --git a/app/utils/dictionary/demography.ts b/app/utils/dictionary/demography.ts index be609145af5..67932649a0a 100644 --- a/app/utils/dictionary/demography.ts +++ b/app/utils/dictionary/demography.ts @@ -1003,4 +1003,4 @@ export const countries = [ name : 'Zimbabwe', code : 'ZW' } -]; \ No newline at end of file +]; diff --git a/tests/integration/components/footer-main-test.js b/tests/integration/components/footer-main-test.js index 948891555f7..4ec3d799710 100644 --- a/tests/integration/components/footer-main-test.js +++ b/tests/integration/components/footer-main-test.js @@ -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')); }); }); diff --git a/tests/unit/services/cache-test.ts b/tests/unit/services/cache-test.ts new file mode 100644 index 00000000000..28841f85a8c --- /dev/null +++ b/tests/unit/services/cache-test.ts @@ -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); + }); +}); +