diff --git a/frontend/index.js b/frontend/index.js index b1fe77f3..371019ac 100644 --- a/frontend/index.js +++ b/frontend/index.js @@ -13,6 +13,7 @@ import Flex from 'components/Flex'; import stores from 'stores'; import DocumentsStore from 'stores/DocumentsStore'; import CollectionsStore from 'stores/CollectionsStore'; +import CacheStore from 'stores/CacheStore'; import 'normalize.css/normalize.css'; import 'styles/base.scss'; @@ -58,11 +59,15 @@ const Auth = ({ children }: AuthProps) => { if (!authenticatedStores) { // Stores for authenticated user const user = stores.auth.getUserStore(); + const cache = new CacheStore(user.user.id); authenticatedStores = { user, - documents: new DocumentsStore(), + documents: new DocumentsStore({ + cache, + }), collections: new CollectionsStore({ teamId: user.team.id, + cache, }), }; diff --git a/frontend/models/Collection.js b/frontend/models/Collection.js index 3215b225..70036fba 100644 --- a/frontend/models/Collection.js +++ b/frontend/models/Collection.js @@ -12,6 +12,7 @@ class Collection { isSaving: boolean = false; hasPendingChanges: boolean = false; errors: ErrorsStore; + data: Object; createdAt: string; description: ?string; @@ -64,10 +65,8 @@ class Collection { }); } invariant(res && res.data, 'Data should be available'); - this.updateData({ - ...res.data, - hasPendingChanges: false, - }); + this.updateData(res.data); + this.hasPendingChanges = false; } catch (e) { this.errors.add('Collection failed saving'); return false; @@ -79,6 +78,7 @@ class Collection { }; updateData(data: Object = {}) { + this.data = data; extendObservable(this, data); } diff --git a/frontend/models/Document.js b/frontend/models/Document.js index e244a976..2755820e 100644 --- a/frontend/models/Document.js +++ b/frontend/models/Document.js @@ -39,6 +39,8 @@ class Document { url: string; views: number; + data: Object; + /* Computed */ @computed get modifiedSinceViewed(): boolean { @@ -148,8 +150,8 @@ class Document { invariant(res && res.data, 'Data should be available'); this.updateData({ ...res.data, - hasPendingChanges: false, }); + this.hasPendingChanges = false; } catch (e) { this.errors.add('Document failed saving'); } finally { @@ -161,7 +163,8 @@ class Document { updateData(data: Object = {}, dirty: boolean = false) { if (data.text) data.title = parseHeader(data.text); - if (dirty) data.hasPendingChanges = true; + if (dirty) this.hasPendingChanges = true; + this.data = data; extendObservable(this, data); } diff --git a/frontend/stores/CacheStore.js b/frontend/stores/CacheStore.js new file mode 100644 index 00000000..0c3270d6 --- /dev/null +++ b/frontend/stores/CacheStore.js @@ -0,0 +1,28 @@ +// @flow +import localForage from 'localforage'; + +class CacheStore { + key: string; + + cacheKey = (key: string): string => { + return `CACHE_${this.key}_${key}`; + }; + + getItem = (key: string): any => { + return localForage.getItem(this.cacheKey(key)); + }; + + setItem = (key: string, value: any): any => { + return localForage.setItem(this.cacheKey(key), value); + }; + + removeItem = (key: string) => { + return localForage.removeItem(this.cacheKey(key)); + }; + + constructor(cacheKey: string) { + this.key = cacheKey; + } +} + +export default CacheStore; diff --git a/frontend/stores/CollectionsStore.js b/frontend/stores/CollectionsStore.js index 985bd863..4914dccc 100644 --- a/frontend/stores/CollectionsStore.js +++ b/frontend/stores/CollectionsStore.js @@ -1,5 +1,11 @@ // @flow -import { observable, action, runInAction, ObservableArray } from 'mobx'; +import { + observable, + action, + runInAction, + ObservableArray, + autorunAsync, +} from 'mobx'; import ApiClient, { client } from 'utils/ApiClient'; import _ from 'lodash'; import invariant from 'invariant'; @@ -7,9 +13,13 @@ import invariant from 'invariant'; import stores from 'stores'; import Collection from 'models/Collection'; import ErrorsStore from 'stores/ErrorsStore'; +import CacheStore from 'stores/CacheStore'; + +const COLLECTION_CACHE_KEY = 'COLLECTION_CACHE_KEY'; type Options = { teamId: string, + cache: CacheStore, }; class CollectionsStore { @@ -19,6 +29,7 @@ class CollectionsStore { client: ApiClient; teamId: string; errors: ErrorsStore; + cache: CacheStore; /* Actions */ @@ -54,6 +65,22 @@ class CollectionsStore { this.client = client; this.errors = stores.errors; this.teamId = options.teamId; + this.cache = options.cache; + + this.cache.getItem(COLLECTION_CACHE_KEY).then(data => { + if (data) { + this.data.replace(data.map(collection => new Collection(collection))); + this.isLoaded = true; + } + }); + + autorunAsync('CollectionsStore.persists', () => { + if (this.data.length > 0) + this.cache.setItem( + COLLECTION_CACHE_KEY, + this.data.map(collection => collection.data) + ); + }); } } diff --git a/frontend/stores/DocumentsStore.js b/frontend/stores/DocumentsStore.js index 649f66ad..b303f7d5 100644 --- a/frontend/stores/DocumentsStore.js +++ b/frontend/stores/DocumentsStore.js @@ -1,5 +1,12 @@ // @flow -import { observable, action, computed, ObservableMap, runInAction } from 'mobx'; +import { + observable, + action, + computed, + ObservableMap, + runInAction, + autorunAsync, +} from 'mobx'; import { client } from 'utils/ApiClient'; import _ from 'lodash'; import invariant from 'invariant'; @@ -7,12 +14,17 @@ import invariant from 'invariant'; import stores from 'stores'; import Document from 'models/Document'; import ErrorsStore from 'stores/ErrorsStore'; +import CacheStore from 'stores/CacheStore'; + +const DOCUMENTS_CACHE_KEY = 'DOCUMENTS_CACHE_KEY'; class DocumentsStore { @observable recentlyViewedIds: Array = []; @observable data: Map = new ObservableMap([]); @observable isLoaded: boolean = false; + errors: ErrorsStore; + cache: CacheStore; /* Computed */ @@ -96,8 +108,24 @@ class DocumentsStore { return _.find(this.data.values(), { url }); }; - constructor() { + constructor(options: Options) { this.errors = stores.errors; + this.cache = options.cache; + + this.cache.getItem(DOCUMENTS_CACHE_KEY).then(data => { + if (data) { + data.forEach(document => this.add(new Document(document))); + } + }); + + autorunAsync('DocumentsStore.persists', () => { + if (this.data.size) { + this.cache.setItem( + DOCUMENTS_CACHE_KEY, + Array.from(this.data.values()).map(collection => collection.data) + ); + } + }); } } diff --git a/package.json b/package.json index ddaee03b..98c1f5b9 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,7 @@ "koa-mount": "^3.0.0", "koa-router": "7.0.1", "koa-sendfile": "2.0.0", + "localforage": "^1.5.0", "lodash": "^4.17.4", "lodash.orderby": "4.4.0", "marked": "0.3.6", diff --git a/webpack.config.js b/webpack.config.js index a0e5c172..91cdf941 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -37,6 +37,8 @@ module.exports = { }, { test: /\.md/, loader: 'raw-loader' }, ], + // Silence warning https://github.com/localForage/localForage/issues/599 + noParse: [new RegExp('node_modules/localforage/dist/localforage.js')], }, resolve: { root: path.join(__dirname, 'frontend'), diff --git a/yarn.lock b/yarn.lock index d76cbf87..5422feaa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -44,6 +44,10 @@ acorn-jsx@^3.0.0, acorn-jsx@^3.0.1: dependencies: acorn "^3.0.4" +acorn@^1.0.3: + version "1.2.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-1.2.2.tgz#c8ce27de0acc76d896d2b1fad3df588d9e82f014" + acorn@^3.0.0, acorn@^3.0.4: version "3.3.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" @@ -264,6 +268,10 @@ ast-types-flow@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" +ast-types@0.8.15: + version "0.8.15" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.8.15.tgz#8eef0827f04dff0ec8857ba925abe3fea6194e52" + ast-types@0.9.8: version "0.9.8" resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.8.tgz#6cb6a40beba31f49f20928e28439fc14a3dab078" @@ -977,6 +985,10 @@ balanced-match@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.1.0.tgz#b504bd05869b39259dd0c5efc35d843176dccc4a" +base62@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/base62/-/base62-0.1.1.tgz#7b4174c2f94449753b11c2651c083da841a7b084" + base64-js@^1.0.2: version "1.2.0" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.0.tgz#a39992d723584811982be5e290bb6a53d86700f1" @@ -2612,6 +2624,14 @@ es-to-primitive@^1.1.1: is-date-object "^1.0.1" is-symbol "^1.0.1" +es3ify@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/es3ify/-/es3ify-0.1.4.tgz#ad9fa5df1ae34f3f31e1211b5818b2d51078dfd1" + dependencies: + esprima-fb "~3001.0001.0000-dev-harmony-fb" + jstransform "~3.0.0" + through "~2.3.4" + es5-ext@^0.10.12, es5-ext@^0.10.7, es5-ext@^0.10.8, es5-ext@^0.10.9, es5-ext@~0.10.11: version "0.10.12" resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.12.tgz#aa84641d4db76b62abba5e45fd805ecbab140047" @@ -2831,6 +2851,10 @@ eslint@^3.19.0: text-table "~0.2.0" user-home "^2.0.0" +esmangle-evaluator@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/esmangle-evaluator/-/esmangle-evaluator-1.0.1.tgz#620d866ef4861b3311f75766d52a8572bb3c6336" + espree@^3.4.0: version "3.4.2" resolved "https://registry.yarnpkg.com/espree/-/espree-3.4.2.tgz#38dbdedbedc95b8961a1fbf04734a8f6a9c8c592" @@ -2838,6 +2862,14 @@ espree@^3.4.0: acorn "^5.0.1" acorn-jsx "^3.0.0" +esprima-fb@~15001.1001.0-dev-harmony-fb: + version "15001.1001.0-dev-harmony-fb" + resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-15001.1001.0-dev-harmony-fb.tgz#43beb57ec26e8cf237d3dd8b33e42533577f2659" + +esprima-fb@~3001.0001.0000-dev-harmony-fb, esprima-fb@~3001.1.0-dev-harmony-fb: + version "3001.1.0-dev-harmony-fb" + resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-3001.0001.0000-dev-harmony-fb.tgz#b77d37abcd38ea0b77426bb8bc2922ce6b426411" + esprima@^2.6.0, esprima@^2.7.1, esprima@~2.7.0: version "2.7.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" @@ -3003,6 +3035,15 @@ extsprintf@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" +falafel@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/falafel/-/falafel-1.2.0.tgz#c18d24ef5091174a497f318cd24b026a25cddab4" + dependencies: + acorn "^1.0.3" + foreach "^2.0.5" + isarray "0.0.1" + object-keys "^1.0.6" + fancy-log@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.2.0.tgz#d5a51b53e9ab22ca07d558f2b67ae55fdb5fcbd8" @@ -3980,6 +4021,10 @@ image-to-data-uri@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/image-to-data-uri/-/image-to-data-uri-1.1.0.tgz#23f9d7f17b6562ca6a8145e9779c9a166b829f6e" +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + immutable@^3.7.6, immutable@^3.8.1: version "3.8.1" resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.1.tgz#200807f11ab0f72710ea485542de088075f68cd2" @@ -4060,6 +4105,13 @@ ini@^1.3.4, ini@~1.3.0: version "1.3.4" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" +inline-process-browser@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/inline-process-browser/-/inline-process-browser-1.0.0.tgz#46a61b153dd3c9b1624b1a00626edb4f7f414f22" + dependencies: + falafel "^1.0.1" + through2 "^0.6.5" + inline-style-prefixer@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-2.0.5.tgz#c153c7e88fd84fef5c602e95a8168b2770671fe7" @@ -4847,6 +4899,14 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.3.6" +jstransform@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/jstransform/-/jstransform-3.0.0.tgz#a2591ab6cee8d97bf3be830dbfa2313b87cd640b" + dependencies: + base62 "0.1.1" + esprima-fb "~3001.1.0-dev-harmony-fb" + source-map "0.1.31" + jsx-ast-utils@^1.0.0: version "1.3.2" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.3.2.tgz#dff658782705352111f9865d40471bc4a955961e" @@ -5068,6 +5128,15 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +lie@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.0.2.tgz#ffda21d7bba26f377cad865d3649b2fc8ce39fea" + dependencies: + es3ify "^0.1.3" + immediate "~3.0.5" + inline-process-browser "^1.0.0" + unreachable-branch-transform "^0.3.0" + liftoff@^2.1.0: version "2.3.0" resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-2.3.0.tgz#a98f2ff67183d8ba7cfaca10548bd7ff0550b385" @@ -5159,6 +5228,12 @@ loader-utils@0.2.x, loader-utils@^0.2.11, loader-utils@^0.2.14, loader-utils@^0. json5 "^0.5.0" object-assign "^4.0.1" +localforage@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.5.0.tgz#6b994e19b56611fa85df3992df397ac4ab66e815" + dependencies: + lie "3.0.2" + locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -6245,7 +6320,7 @@ object-is@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6" -object-keys@^1.0.10, object-keys@^1.0.8: +object-keys@^1.0.10, object-keys@^1.0.6, object-keys@^1.0.8: version "1.0.11" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" @@ -6968,7 +7043,7 @@ prismjs@^1.6.0: optionalDependencies: clipboard "^1.5.5" -private@^0.1.6: +private@^0.1.6, private@~0.1.5: version "0.1.6" resolved "https://registry.yarnpkg.com/private/-/private-0.1.6.tgz#55c6a976d0f9bafb9924851350fe47b9b5fbb7c1" @@ -7335,6 +7410,15 @@ readline2@^1.0.1: is-fullwidth-code-point "^1.0.0" mute-stream "0.0.5" +recast@^0.10.1: + version "0.10.43" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.10.43.tgz#b95d50f6d60761a5f6252e15d80678168491ce7f" + dependencies: + ast-types "0.8.15" + esprima-fb "~15001.1001.0-dev-harmony-fb" + private "~0.1.5" + source-map "~0.5.0" + rechoir@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -8033,6 +8117,12 @@ source-map-support@^0.4.2: dependencies: source-map "^0.5.3" +source-map@0.1.31: + version "0.1.31" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.31.tgz#9f704d0d69d9e138a81badf6ebb4fde33d151c61" + dependencies: + amdefine ">=0.0.4" + source-map@0.1.x: version "0.1.43" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346" @@ -8045,7 +8135,7 @@ source-map@0.4.x, source-map@^0.4.4, source-map@~0.4.1: dependencies: amdefine ">=0.0.4" -source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3: +source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.3: version "0.5.6" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" @@ -8395,7 +8485,7 @@ throat@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/throat/-/throat-3.0.0.tgz#e7c64c867cbb3845f10877642f7b60055b8ec0d6" -through2@^0.6.1: +through2@^0.6.1, through2@^0.6.2, through2@^0.6.5: version "0.6.5" resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48" dependencies: @@ -8409,7 +8499,7 @@ through2@^2.0.0: readable-stream "~2.0.0" xtend "~4.0.0" -through@2, through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.1: +through@2, through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.1, through@~2.3.4: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -8665,6 +8755,14 @@ unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" +unreachable-branch-transform@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/unreachable-branch-transform/-/unreachable-branch-transform-0.3.0.tgz#d99cc4c6e746d264928845b611db54b0f3474caa" + dependencies: + esmangle-evaluator "^1.0.0" + recast "^0.10.1" + through2 "^0.6.2" + unzip@^0.1.11: version "0.1.11" resolved "https://registry.yarnpkg.com/unzip/-/unzip-0.1.11.tgz#89749c63b058d7d90d619f86b98aa1535d3b97f0"