From 9719496209ee313c1bff81ba2713ec6bc3b3c038 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Mon, 1 Aug 2016 19:03:17 +0300 Subject: [PATCH] Simple cache store --- frontend/scenes/Dashboard/DashboardStore.js | 13 +++--- .../scenes/DocumentEdit/DocumentEditStore.js | 10 ++--- .../scenes/DocumentScene/DocumentScene.js | 10 ++++- .../DocumentScene/DocumentSceneStore.js | 34 +++++++++------ frontend/stores/CacheStore.js | 42 +++++++++++++++++++ frontend/stores/UiStore.js | 2 +- frontend/stores/index.js | 7 +++- frontend/utils/ApiClient.js | 33 +++++++++++---- 8 files changed, 115 insertions(+), 36 deletions(-) create mode 100644 frontend/stores/CacheStore.js diff --git a/frontend/scenes/Dashboard/DashboardStore.js b/frontend/scenes/Dashboard/DashboardStore.js index 4bedb05ae..f4f8877bf 100644 --- a/frontend/scenes/Dashboard/DashboardStore.js +++ b/frontend/scenes/Dashboard/DashboardStore.js @@ -1,5 +1,5 @@ -import { observable, action } from 'mobx'; -import { client } from 'utils/ApiClient'; +import { observable, action, runInAction } from 'mobx'; +import { client, cacheResponse } from 'utils/ApiClient'; const store = new class DashboardStore { @observable atlases; @@ -15,8 +15,11 @@ const store = new class DashboardStore { try { const res = await client.post('/atlases.list', { id: teamId }); const { data, pagination } = res; - this.atlases = data; - this.pagination = pagination; + runInAction('fetchAtlases', () => { + this.atlases = data; + this.pagination = pagination; + data.forEach((collection) => cacheResponse(collection.recentDocuments)); + }); } catch (e) { console.error("Something went wrong"); } @@ -24,4 +27,4 @@ const store = new class DashboardStore { } }(); -export default store; \ No newline at end of file +export default store; diff --git a/frontend/scenes/DocumentEdit/DocumentEditStore.js b/frontend/scenes/DocumentEdit/DocumentEditStore.js index 9471eb702..348d2769f 100644 --- a/frontend/scenes/DocumentEdit/DocumentEditStore.js +++ b/frontend/scenes/DocumentEdit/DocumentEditStore.js @@ -41,7 +41,7 @@ class DocumentEditStore { try { const data = await client.get('/documents.info', { id: this.documentId, - }); + }, { cache: true }); if (this.newChildDocument) { this.parentDocument = data.data; } else { @@ -66,7 +66,7 @@ class DocumentEditStore { atlas: this.atlasId || this.parentDocument.atlas.id, title: this.title, text: this.text, - }); + }, { cache: true }); const { id } = data.data; this.hasPendingChanges = false; @@ -87,7 +87,7 @@ class DocumentEditStore { id: this.documentId, title: this.title, text: this.text, - }); + }, { cache: true }); this.hasPendingChanges = false; browserHistory.push(`/documents/${this.documentId}`); @@ -131,7 +131,7 @@ class DocumentEditStore { constructor(settings) { // Rehydrate settings - this.preview = settings.preview + this.preview = settings.preview; // Persist settings to localStorage // TODO: This could be done more selectively @@ -139,7 +139,7 @@ class DocumentEditStore { this.persistSettings(); }); } -}; +} export default DocumentEditStore; export { diff --git a/frontend/scenes/DocumentScene/DocumentScene.js b/frontend/scenes/DocumentScene/DocumentScene.js index c7df607eb..2edc57b24 100644 --- a/frontend/scenes/DocumentScene/DocumentScene.js +++ b/frontend/scenes/DocumentScene/DocumentScene.js @@ -24,10 +24,11 @@ const cx = classNames.bind(styles); import treeStyles from 'components/Tree/Tree.scss'; @keydown(['cmd+/', 'ctrl+/', 'c', 'e']) -@observer(['ui']) +@observer(['ui', 'cache']) class DocumentScene extends React.Component { static propTypes = { ui: PropTypes.object.isRequired, + cache: PropTypes.object.isRequired, } static store; @@ -38,7 +39,12 @@ class DocumentScene extends React.Component { constructor(props) { super(props); - this.store = new DocumentSceneStore(JSON.parse(localStorage[DOCUMENT_PREFERENCES] || "{}")); + this.store = new DocumentSceneStore( + JSON.parse(localStorage[DOCUMENT_PREFERENCES] || "{}"), + { + cache: this.props.cache, + } + ); } componentDidMount = () => { diff --git a/frontend/scenes/DocumentScene/DocumentSceneStore.js b/frontend/scenes/DocumentScene/DocumentSceneStore.js index b93837c88..9c4be624f 100644 --- a/frontend/scenes/DocumentScene/DocumentSceneStore.js +++ b/frontend/scenes/DocumentScene/DocumentSceneStore.js @@ -1,17 +1,19 @@ import _isEqual from 'lodash/isEqual'; import _indexOf from 'lodash/indexOf'; import _without from 'lodash/without'; -import { observable, action, computed, runInAction, toJS, autorun } from 'mobx'; +import { observable, action, computed, runInAction, toJS, autorunAsync } from 'mobx'; import { client } from 'utils/ApiClient'; import { browserHistory } from 'react-router'; const DOCUMENT_PREFERENCES = 'DOCUMENT_PREFERENCES'; class DocumentSceneStore { + static cache; + @observable document; @observable collapsedNodes = []; - @observable isFetching = true; + @observable isFetching; @observable updatingContent = false; @observable updatingStructure = false; @observable isDeleting; @@ -24,8 +26,8 @@ class DocumentSceneStore { } @computed get atlasTree() { - if (this.document.atlas.type !== 'atlas') return; - let tree = this.document.atlas.navigationTree; + if (!this.document || this.document.atlas.type !== 'atlas') return; + const tree = this.document.atlas.navigationTree; const collapseNodes = (node) => { if (this.collapsedNodes.includes(node.id)) { @@ -43,12 +45,19 @@ class DocumentSceneStore { /* Actions */ - @action fetchDocument = async (id, softLoad) => { + @action fetchDocument = async (id, softLoad = false) => { + let cacheHit = false; + runInAction('retrieve document from cache', () => { + const cachedValue = this.cache.fetchFromCache(id); + cacheHit = !!cachedValue; + if (cacheHit) this.document = cachedValue; + }); + this.isFetching = !softLoad; - this.updatingContent = softLoad; + this.updatingContent = softLoad && !cacheHit; try { - const res = await client.get('/documents.info', { id: id }); + const res = await client.get('/documents.info', { id }, { cache: true }); const { data } = res; runInAction('fetchDocument', () => { this.document = data; @@ -64,7 +73,7 @@ class DocumentSceneStore { this.isFetching = true; try { - const res = await client.post('/documents.delete', { id: this.document.id }); + await client.post('/documents.delete', { id: this.document.id }); browserHistory.push(`/atlas/${this.document.atlas.id}`); } catch (e) { console.error("Something went wrong"); @@ -83,7 +92,7 @@ class DocumentSceneStore { try { const res = await client.post('/atlases.updateNavigationTree', { id: this.document.atlas.id, - tree: tree, + tree, }); runInAction('updateNavigationTree', () => { const { data } = res; @@ -111,17 +120,18 @@ class DocumentSceneStore { }); } - constructor(settings) { + constructor(settings, options) { // Rehydrate settings this.collapsedNodes = settings.collapsedNodes || []; + this.cache = options.cache; // Persist settings to localStorage // TODO: This could be done more selectively - autorun(() => { + autorunAsync(() => { this.persistSettings(); }); } -}; +} export default DocumentSceneStore; export { diff --git a/frontend/stores/CacheStore.js b/frontend/stores/CacheStore.js new file mode 100644 index 000000000..75e885c88 --- /dev/null +++ b/frontend/stores/CacheStore.js @@ -0,0 +1,42 @@ +import _ from 'lodash'; +import { action, toJS } from 'mobx'; + +const CACHE_STORE = 'CACHE_STORE'; + +class CacheStore { + cache = {}; + + /* Computed */ + + get asJson() { + return JSON.stringify({ + cache: this.cache, + }); + } + + /* Actions */ + + @action cacheWithId = (id, data) => { + this.cache[id] = toJS(data); + _.defer(() => localStorage.setItem(CACHE_STORE, this.asJson)); + }; + + @action cacheList = (data) => { + data.forEach((item) => this.cacheWithId(item.id, item)); + }; + + @action fetchFromCache = (id) => { + return this.cache[id]; + } + + constructor() { + // Rehydrate + const data = JSON.parse(localStorage.getItem(CACHE_STORE) || '{}'); + this.cache = data.cache || {}; + } +} + +export default CacheStore; +export { + CACHE_STORE, +}; diff --git a/frontend/stores/UiStore.js b/frontend/stores/UiStore.js index d914f7df6..4b837da58 100644 --- a/frontend/stores/UiStore.js +++ b/frontend/stores/UiStore.js @@ -24,7 +24,7 @@ class UiStore { const data = JSON.parse(localStorage.getItem(UI_STORE) || '{}'); this.sidebar = data.sidebar; } -}; +} export default UiStore; export { diff --git a/frontend/stores/index.js b/frontend/stores/index.js index df8cddcf7..ab1e9cc90 100644 --- a/frontend/stores/index.js +++ b/frontend/stores/index.js @@ -1,14 +1,17 @@ import UserStore, { USER_STORE } from './UserStore'; import UiStore, { UI_STORE } from './UiStore'; -import { autorun, toJS } from 'mobx'; +import CacheStore from './CacheStore'; +import { autorunAsync } from 'mobx'; const stores = { user: new UserStore(), ui: new UiStore(), + cache: new CacheStore(), }; // Persist stores to localStorage -autorun(() => { +// TODO: move to store constructors +autorunAsync(() => { localStorage.setItem(USER_STORE, stores.user.asJson); localStorage.setItem(UI_STORE, stores.ui.asJson); }); diff --git a/frontend/utils/ApiClient.js b/frontend/utils/ApiClient.js index 417ef1d02..8616b9674 100644 --- a/frontend/utils/ApiClient.js +++ b/frontend/utils/ApiClient.js @@ -1,15 +1,26 @@ -import _map from 'lodash/map'; +import _ from 'lodash'; import stores from 'stores'; import constants from '../constants'; +const isIterable = object => + object != null && typeof object[Symbol.iterator] === 'function'; + +const cacheResponse = (data) => { + if (isIterable(data)) { + stores.cache.cacheList(data); + } else { + stores.cache.cacheWithId(data.id, data); + } +}; + class ApiClient { constructor(options = {}) { this.baseUrl = options.baseUrl || constants.API_BASE_URL; this.userAgent = options.userAgent || constants.API_USER_AGENT; } - fetch = (path, method, data) => { + fetch = (path, method, data, options = {}) => { let body; let modifiedPath; @@ -63,12 +74,16 @@ class ApiClient { error.statusCode = response.status; error.response = response; - reject(error); + return reject(error); }) .then((response) => { return response.json(); }) .then((json) => { + // Cache responses + if (options.cache) { + cacheResponse(json.data); + } resolve(json); }) .catch(() => { @@ -77,18 +92,18 @@ class ApiClient { }); } - get = (path, data) => { - return this.fetch(path, 'GET', data); + get = (path, data, options) => { + return this.fetch(path, 'GET', data, options); } - post = (path, data) => { - return this.fetch(path, 'POST', data); + post = (path, data, options) => { + return this.fetch(path, 'POST', data, options); } // Helpers constructQueryString = (data) => { - return _map(data, (v, k) => { + return _.map(data, (v, k) => { return `${encodeURIComponent(k)}=${encodeURIComponent(v)}`; }).join('&'); }; @@ -98,4 +113,4 @@ export default ApiClient; // In case you don't want to always initiate, just import with `import { client } ...` const client = new ApiClient(); -export { client }; +export { client, cacheResponse };