Simple cache store
This commit is contained in:
@ -1,5 +1,5 @@
|
|||||||
import { observable, action } from 'mobx';
|
import { observable, action, runInAction } from 'mobx';
|
||||||
import { client } from 'utils/ApiClient';
|
import { client, cacheResponse } from 'utils/ApiClient';
|
||||||
|
|
||||||
const store = new class DashboardStore {
|
const store = new class DashboardStore {
|
||||||
@observable atlases;
|
@observable atlases;
|
||||||
@ -15,8 +15,11 @@ const store = new class DashboardStore {
|
|||||||
try {
|
try {
|
||||||
const res = await client.post('/atlases.list', { id: teamId });
|
const res = await client.post('/atlases.list', { id: teamId });
|
||||||
const { data, pagination } = res;
|
const { data, pagination } = res;
|
||||||
|
runInAction('fetchAtlases', () => {
|
||||||
this.atlases = data;
|
this.atlases = data;
|
||||||
this.pagination = pagination;
|
this.pagination = pagination;
|
||||||
|
data.forEach((collection) => cacheResponse(collection.recentDocuments));
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Something went wrong");
|
console.error("Something went wrong");
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ class DocumentEditStore {
|
|||||||
try {
|
try {
|
||||||
const data = await client.get('/documents.info', {
|
const data = await client.get('/documents.info', {
|
||||||
id: this.documentId,
|
id: this.documentId,
|
||||||
});
|
}, { cache: true });
|
||||||
if (this.newChildDocument) {
|
if (this.newChildDocument) {
|
||||||
this.parentDocument = data.data;
|
this.parentDocument = data.data;
|
||||||
} else {
|
} else {
|
||||||
@ -66,7 +66,7 @@ class DocumentEditStore {
|
|||||||
atlas: this.atlasId || this.parentDocument.atlas.id,
|
atlas: this.atlasId || this.parentDocument.atlas.id,
|
||||||
title: this.title,
|
title: this.title,
|
||||||
text: this.text,
|
text: this.text,
|
||||||
});
|
}, { cache: true });
|
||||||
const { id } = data.data;
|
const { id } = data.data;
|
||||||
|
|
||||||
this.hasPendingChanges = false;
|
this.hasPendingChanges = false;
|
||||||
@ -87,7 +87,7 @@ class DocumentEditStore {
|
|||||||
id: this.documentId,
|
id: this.documentId,
|
||||||
title: this.title,
|
title: this.title,
|
||||||
text: this.text,
|
text: this.text,
|
||||||
});
|
}, { cache: true });
|
||||||
|
|
||||||
this.hasPendingChanges = false;
|
this.hasPendingChanges = false;
|
||||||
browserHistory.push(`/documents/${this.documentId}`);
|
browserHistory.push(`/documents/${this.documentId}`);
|
||||||
@ -131,7 +131,7 @@ class DocumentEditStore {
|
|||||||
|
|
||||||
constructor(settings) {
|
constructor(settings) {
|
||||||
// Rehydrate settings
|
// Rehydrate settings
|
||||||
this.preview = settings.preview
|
this.preview = settings.preview;
|
||||||
|
|
||||||
// Persist settings to localStorage
|
// Persist settings to localStorage
|
||||||
// TODO: This could be done more selectively
|
// TODO: This could be done more selectively
|
||||||
@ -139,7 +139,7 @@ class DocumentEditStore {
|
|||||||
this.persistSettings();
|
this.persistSettings();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default DocumentEditStore;
|
export default DocumentEditStore;
|
||||||
export {
|
export {
|
||||||
|
@ -24,10 +24,11 @@ const cx = classNames.bind(styles);
|
|||||||
import treeStyles from 'components/Tree/Tree.scss';
|
import treeStyles from 'components/Tree/Tree.scss';
|
||||||
|
|
||||||
@keydown(['cmd+/', 'ctrl+/', 'c', 'e'])
|
@keydown(['cmd+/', 'ctrl+/', 'c', 'e'])
|
||||||
@observer(['ui'])
|
@observer(['ui', 'cache'])
|
||||||
class DocumentScene extends React.Component {
|
class DocumentScene extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
ui: PropTypes.object.isRequired,
|
ui: PropTypes.object.isRequired,
|
||||||
|
cache: PropTypes.object.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
static store;
|
static store;
|
||||||
@ -38,7 +39,12 @@ class DocumentScene extends React.Component {
|
|||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(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 = () => {
|
componentDidMount = () => {
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
import _isEqual from 'lodash/isEqual';
|
import _isEqual from 'lodash/isEqual';
|
||||||
import _indexOf from 'lodash/indexOf';
|
import _indexOf from 'lodash/indexOf';
|
||||||
import _without from 'lodash/without';
|
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 { client } from 'utils/ApiClient';
|
||||||
import { browserHistory } from 'react-router';
|
import { browserHistory } from 'react-router';
|
||||||
|
|
||||||
const DOCUMENT_PREFERENCES = 'DOCUMENT_PREFERENCES';
|
const DOCUMENT_PREFERENCES = 'DOCUMENT_PREFERENCES';
|
||||||
|
|
||||||
class DocumentSceneStore {
|
class DocumentSceneStore {
|
||||||
|
static cache;
|
||||||
|
|
||||||
@observable document;
|
@observable document;
|
||||||
@observable collapsedNodes = [];
|
@observable collapsedNodes = [];
|
||||||
|
|
||||||
@observable isFetching = true;
|
@observable isFetching;
|
||||||
@observable updatingContent = false;
|
@observable updatingContent = false;
|
||||||
@observable updatingStructure = false;
|
@observable updatingStructure = false;
|
||||||
@observable isDeleting;
|
@observable isDeleting;
|
||||||
@ -24,8 +26,8 @@ class DocumentSceneStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@computed get atlasTree() {
|
@computed get atlasTree() {
|
||||||
if (this.document.atlas.type !== 'atlas') return;
|
if (!this.document || this.document.atlas.type !== 'atlas') return;
|
||||||
let tree = this.document.atlas.navigationTree;
|
const tree = this.document.atlas.navigationTree;
|
||||||
|
|
||||||
const collapseNodes = (node) => {
|
const collapseNodes = (node) => {
|
||||||
if (this.collapsedNodes.includes(node.id)) {
|
if (this.collapsedNodes.includes(node.id)) {
|
||||||
@ -43,12 +45,19 @@ class DocumentSceneStore {
|
|||||||
|
|
||||||
/* Actions */
|
/* 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.isFetching = !softLoad;
|
||||||
this.updatingContent = softLoad;
|
this.updatingContent = softLoad && !cacheHit;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await client.get('/documents.info', { id: id });
|
const res = await client.get('/documents.info', { id }, { cache: true });
|
||||||
const { data } = res;
|
const { data } = res;
|
||||||
runInAction('fetchDocument', () => {
|
runInAction('fetchDocument', () => {
|
||||||
this.document = data;
|
this.document = data;
|
||||||
@ -64,7 +73,7 @@ class DocumentSceneStore {
|
|||||||
this.isFetching = true;
|
this.isFetching = true;
|
||||||
|
|
||||||
try {
|
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}`);
|
browserHistory.push(`/atlas/${this.document.atlas.id}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Something went wrong");
|
console.error("Something went wrong");
|
||||||
@ -83,7 +92,7 @@ class DocumentSceneStore {
|
|||||||
try {
|
try {
|
||||||
const res = await client.post('/atlases.updateNavigationTree', {
|
const res = await client.post('/atlases.updateNavigationTree', {
|
||||||
id: this.document.atlas.id,
|
id: this.document.atlas.id,
|
||||||
tree: tree,
|
tree,
|
||||||
});
|
});
|
||||||
runInAction('updateNavigationTree', () => {
|
runInAction('updateNavigationTree', () => {
|
||||||
const { data } = res;
|
const { data } = res;
|
||||||
@ -111,17 +120,18 @@ class DocumentSceneStore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(settings) {
|
constructor(settings, options) {
|
||||||
// Rehydrate settings
|
// Rehydrate settings
|
||||||
this.collapsedNodes = settings.collapsedNodes || [];
|
this.collapsedNodes = settings.collapsedNodes || [];
|
||||||
|
this.cache = options.cache;
|
||||||
|
|
||||||
// Persist settings to localStorage
|
// Persist settings to localStorage
|
||||||
// TODO: This could be done more selectively
|
// TODO: This could be done more selectively
|
||||||
autorun(() => {
|
autorunAsync(() => {
|
||||||
this.persistSettings();
|
this.persistSettings();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default DocumentSceneStore;
|
export default DocumentSceneStore;
|
||||||
export {
|
export {
|
||||||
|
42
frontend/stores/CacheStore.js
Normal file
42
frontend/stores/CacheStore.js
Normal file
@ -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,
|
||||||
|
};
|
@ -24,7 +24,7 @@ class UiStore {
|
|||||||
const data = JSON.parse(localStorage.getItem(UI_STORE) || '{}');
|
const data = JSON.parse(localStorage.getItem(UI_STORE) || '{}');
|
||||||
this.sidebar = data.sidebar;
|
this.sidebar = data.sidebar;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default UiStore;
|
export default UiStore;
|
||||||
export {
|
export {
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
import UserStore, { USER_STORE } from './UserStore';
|
import UserStore, { USER_STORE } from './UserStore';
|
||||||
import UiStore, { UI_STORE } from './UiStore';
|
import UiStore, { UI_STORE } from './UiStore';
|
||||||
import { autorun, toJS } from 'mobx';
|
import CacheStore from './CacheStore';
|
||||||
|
import { autorunAsync } from 'mobx';
|
||||||
|
|
||||||
const stores = {
|
const stores = {
|
||||||
user: new UserStore(),
|
user: new UserStore(),
|
||||||
ui: new UiStore(),
|
ui: new UiStore(),
|
||||||
|
cache: new CacheStore(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Persist stores to localStorage
|
// Persist stores to localStorage
|
||||||
autorun(() => {
|
// TODO: move to store constructors
|
||||||
|
autorunAsync(() => {
|
||||||
localStorage.setItem(USER_STORE, stores.user.asJson);
|
localStorage.setItem(USER_STORE, stores.user.asJson);
|
||||||
localStorage.setItem(UI_STORE, stores.ui.asJson);
|
localStorage.setItem(UI_STORE, stores.ui.asJson);
|
||||||
});
|
});
|
||||||
|
@ -1,15 +1,26 @@
|
|||||||
import _map from 'lodash/map';
|
import _ from 'lodash';
|
||||||
import stores from 'stores';
|
import stores from 'stores';
|
||||||
|
|
||||||
import constants from '../constants';
|
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 {
|
class ApiClient {
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
this.baseUrl = options.baseUrl || constants.API_BASE_URL;
|
this.baseUrl = options.baseUrl || constants.API_BASE_URL;
|
||||||
this.userAgent = options.userAgent || constants.API_USER_AGENT;
|
this.userAgent = options.userAgent || constants.API_USER_AGENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch = (path, method, data) => {
|
fetch = (path, method, data, options = {}) => {
|
||||||
let body;
|
let body;
|
||||||
let modifiedPath;
|
let modifiedPath;
|
||||||
|
|
||||||
@ -63,12 +74,16 @@ class ApiClient {
|
|||||||
|
|
||||||
error.statusCode = response.status;
|
error.statusCode = response.status;
|
||||||
error.response = response;
|
error.response = response;
|
||||||
reject(error);
|
return reject(error);
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then((json) => {
|
.then((json) => {
|
||||||
|
// Cache responses
|
||||||
|
if (options.cache) {
|
||||||
|
cacheResponse(json.data);
|
||||||
|
}
|
||||||
resolve(json);
|
resolve(json);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@ -77,18 +92,18 @@ class ApiClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get = (path, data) => {
|
get = (path, data, options) => {
|
||||||
return this.fetch(path, 'GET', data);
|
return this.fetch(path, 'GET', data, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
post = (path, data) => {
|
post = (path, data, options) => {
|
||||||
return this.fetch(path, 'POST', data);
|
return this.fetch(path, 'POST', data, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
|
|
||||||
constructQueryString = (data) => {
|
constructQueryString = (data) => {
|
||||||
return _map(data, (v, k) => {
|
return _.map(data, (v, k) => {
|
||||||
return `${encodeURIComponent(k)}=${encodeURIComponent(v)}`;
|
return `${encodeURIComponent(k)}=${encodeURIComponent(v)}`;
|
||||||
}).join('&');
|
}).join('&');
|
||||||
};
|
};
|
||||||
@ -98,4 +113,4 @@ export default ApiClient;
|
|||||||
|
|
||||||
// In case you don't want to always initiate, just import with `import { client } ...`
|
// In case you don't want to always initiate, just import with `import { client } ...`
|
||||||
const client = new ApiClient();
|
const client = new ApiClient();
|
||||||
export { client };
|
export { client, cacheResponse };
|
||||||
|
Reference in New Issue
Block a user