diff --git a/package.json b/package.json index ccf62afd..bc6dfe63 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@mdi/svg": "^5.9.55", "fetch-retry": "^4.1.0", "localforage": "^1.9.0", + "lodash.debounce": "^4.0.8", "roboto-fontface": "^0.10.0", "vue": "^2.6.12", "vue-i18n": "^8.22.4", diff --git a/src/store/albums.js b/src/store/albums.js index d6019078..82341e01 100644 --- a/src/store/albums.js +++ b/src/store/albums.js @@ -108,7 +108,7 @@ export default { async index({ commit, rootState }) { const generator = index(rootState.auth); try { - await store.restored; + await store.albumsRestored; await fetchAll(commit, generator, "setAlbums"); return true; } catch (error) { @@ -129,7 +129,7 @@ export default { async read({ commit, rootState }, id) { try { const result = await read(rootState.auth, id); - await store.restored; + await store.albumsRestored; commit("setAlbum", { id, album: result }); return result.id; } catch (error) { diff --git a/src/store/artists.js b/src/store/artists.js index 20b824d5..c57a621c 100644 --- a/src/store/artists.js +++ b/src/store/artists.js @@ -58,7 +58,7 @@ export default { async index({ commit, rootState }) { const generator = index(rootState.auth); try { - await store.restored; + await store.artistsRestored; await fetchAll(commit, generator, "setArtists"); return true; } catch (error) { @@ -79,7 +79,7 @@ export default { async read({ commit, rootState }, id) { try { const result = await read(rootState.auth, id); - await store.restored; + await store.artistsRestored; commit("setArtist", { id, artist: result }); return result.id; } catch (error) { diff --git a/src/store/genres.js b/src/store/genres.js index 0e9a817a..c3d3e0bb 100644 --- a/src/store/genres.js +++ b/src/store/genres.js @@ -58,7 +58,7 @@ export default { async index({ commit, rootState }) { const generator = index(rootState.auth); try { - await store.restored; + await store.genresRestored; await fetchAll(commit, generator, "setGenres"); return true; } catch (error) { @@ -79,7 +79,7 @@ export default { async read({ commit, rootState }, id) { try { const result = await read(rootState.auth, id); - await store.restored; + await store.genresRestored; commit("setGenre", { id, genre: result }); return result.id; } catch (error) { diff --git a/src/store/labels.js b/src/store/labels.js index ce45377d..9133526d 100644 --- a/src/store/labels.js +++ b/src/store/labels.js @@ -58,7 +58,7 @@ export default { async index({ commit, rootState }) { const generator = index(rootState.auth); try { - await store.restored; + await store.labelsRestored; await fetchAll(commit, generator, "setLabels"); return true; } catch (error) { @@ -79,7 +79,7 @@ export default { async read({ commit, rootState }, id) { try { const result = await read(rootState.auth, id); - await store.restored; + await store.labelsRestored; commit("setLabel", { id, label: result }); return result.id; } catch (error) { diff --git a/src/store/persistence.js b/src/store/persistence.js index 240f0f60..c07a0de4 100644 --- a/src/store/persistence.js +++ b/src/store/persistence.js @@ -1,5 +1,75 @@ import VuexPersistence from "vuex-persist"; import localForage from "localforage"; +import debounce from "lodash.debounce"; + +// A subclass for VuexPersintence to deal with single namepaced modules and large collections +// Should be provided with a module and an async storage +// IMPORTANT: This class assumes a lot about the structure of state and mutations available in each module +// If this structure changes, we inevitably have to update this class +class VuexPersistentModule extends VuexPersistence { + constructor(module, storage) { + // Write startLoading and the main collection (as an array) to storage + async function saveModule(module, state, storage) { + const modifiedState = { + startLoading: state[module].startLoading, + }; + modifiedState[module] = Object.values(state[module][module]); + await storage.setItem(module, modifiedState); + } + + // Set all options in parent class + super({ + storage, + asyncStorage: true, + modules: [module], + key: module, + filter: (m) => m.type.substring(0, m.type.indexOf("/")) === module, + saveState: debounce(saveModule, 60000), + }); + + // Get item from an async storage and commit them back to the store + this.replaceState = async (module, storage, store) => { + const savedState = await storage.getItem(module); + if (savedState) { + if (savedState.startLoading) { + store.commit(`${module}/setStartLoading`, savedState.startLoading); + } + if (savedState[module]) { + store.commit( + `${module}/set${module.charAt(0).toUpperCase() + module.slice(1)}`, + savedState[module] + ); + } + } + }; + + // Overwrite plugin to use replaceState and set a custom restored prop for each module + this.plugin = (store) => { + store[`${module}Restored`] = this.replaceState( + this.key, + this.storage, + store + ).then(() => { + // The subscriber is the same as in VuexPersistence, but we have to add it ourselves since we overwrite the plugin method + this.subscriber(store)((mutation, state) => { + if (this.filter(mutation)) { + this._mutex.enqueue( + this.saveState(this.key, this.reducer(state), this.storage) + ); + } + }); + this.subscribed = true; + }); + }; + } +} + +const plugins = ["albums", "artists", "genres", "labels", "tracks"].map( + (module) => { + const plugin = new VuexPersistentModule(module, localForage); + return plugin.plugin; + } +); const localStorageModules = [ "auth", @@ -12,16 +82,6 @@ const localStorageModules = [ "users", "userSettings", ]; -const localForageModules = ["albums", "artists", "genres", "labels"]; - -export const vuexLocalForage = new VuexPersistence({ - storage: localForage, - asyncStorage: true, - strictMode: process.env.NODE_ENV !== "production", - modules: localForageModules, - filter: (m) => - localForageModules.includes(m.type.substring(0, m.type.indexOf("/"))), -}); export const vuexLocalStorage = new VuexPersistence({ storage: window.localStorage, @@ -30,3 +90,7 @@ export const vuexLocalStorage = new VuexPersistence({ filter: (m) => localStorageModules.includes(m.type.substring(0, m.type.indexOf("/"))), }); + +plugins.push(vuexLocalStorage.plugin); + +export default plugins; diff --git a/src/store/store.js b/src/store/store.js index 9c578880..1eddebc6 100644 --- a/src/store/store.js +++ b/src/store/store.js @@ -1,6 +1,6 @@ import Vue from "vue"; import Vuex from "vuex"; -import { vuexLocalForage, vuexLocalStorage } from "./persistence"; +import persistencePlugins, { vuexLocalStorage } from "./persistence"; import albums from "./albums"; import artists from "./artists"; import auth from "./auth"; @@ -35,11 +35,11 @@ const mutations = { }; if (process.env.NODE_ENV !== "production") { - mutations.RESTORE_MUTATION = vuexLocalForage.RESTORE_MUTATION; + mutations.RESTORE_MUTATION = vuexLocalStorage.RESTORE_MUTATION; } export default new Vuex.Store({ - plugins: [vuexLocalForage.plugin, vuexLocalStorage.plugin], + plugins: persistencePlugins, strict: process.env.NODE_ENV !== "production", modules: { albums, diff --git a/src/store/tracks.js b/src/store/tracks.js index 79477c17..fbbe0d3d 100644 --- a/src/store/tracks.js +++ b/src/store/tracks.js @@ -2,6 +2,7 @@ import Vue from "vue"; import { index, create, destroy, update, read, merge } from "../api/tracks"; import { fetchAll } from "./actions"; import { compareTracks } from "../comparators"; +import store from "./store"; export default { namespaced: true, @@ -100,6 +101,7 @@ export default { async index({ commit, rootState }) { const generator = index(rootState.auth); try { + await store.tracksRestored; await fetchAll(commit, generator, "setTracks"); return true; } catch (error) { @@ -120,6 +122,7 @@ export default { async read({ commit, rootState }, id) { try { const track = await read(rootState.auth, id); + await store.tracksRestored; commit("setTrack", { id, track }); return true; } catch (error) { diff --git a/yarn.lock b/yarn.lock index 16f4e740..da0325cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5264,6 +5264,11 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= + lodash.defaultsdeep@^4.6.1: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz#512e9bd721d272d94e3d3a63653fa17516741ca6"