Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache vuex store #415

Merged
merged 12 commits into from
Jul 14, 2021
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"@mdi/font": "^5.9.55",
"@mdi/svg": "^5.9.55",
"fetch-retry": "^4.1.1",
"localforage": "^1.9.0",
"lodash.debounce": "^4.0.8",
"roboto-fontface": "^0.10.0",
"vue": "^2.6.14",
"vue-i18n": "^8.24.5",
Expand All @@ -20,7 +22,7 @@
"vuedraggable": "^2.24.3",
"vuetify": "^2.5.6",
"vuex": "^3.6.2",
"vuex-persistedstate": "^3.2.0"
"vuex-persist": "^3.1.3"
},
"devDependencies": {
"@intlify/vue-i18n-loader": "^1.1.0",
Expand Down
2 changes: 2 additions & 0 deletions src/store/albums.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export default {
async index({ commit, rootState }, scope = new AlbumsScope()) {
const generator = index(rootState.auth, scope);
try {
await this.albumsRestored;
await fetchAll(commit, generator, "setAlbums", scope);
return true;
} catch (error) {
Expand All @@ -128,6 +129,7 @@ export default {
async read({ commit, rootState }, id) {
try {
const result = await read(rootState.auth, id);
await this.albumsRestored;
commit("setAlbum", { id, album: result });
return result.id;
} catch (error) {
Expand Down
2 changes: 2 additions & 0 deletions src/store/artists.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export default {
async index({ commit, rootState }, scope = new ArtistsScope()) {
const generator = index(rootState.auth, scope);
try {
await this.artistsRestored;
await fetchAll(commit, generator, "setArtists", scope);
return true;
} catch (error) {
Expand All @@ -78,6 +79,7 @@ export default {
async read({ commit, rootState }, id) {
try {
const result = await read(rootState.auth, id);
await this.artistsRestored;
commit("setArtist", { id, artist: result });
return result.id;
} catch (error) {
Expand Down
2 changes: 2 additions & 0 deletions src/store/genres.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export default {
async index({ commit, rootState }) {
const generator = index(rootState.auth);
try {
await this.genresRestored;
await fetchAll(commit, generator, "setGenres");
return true;
} catch (error) {
Expand All @@ -77,6 +78,7 @@ export default {
async read({ commit, rootState }, id) {
try {
const result = await read(rootState.auth, id);
await this.genresRestored;
commit("setGenre", { id, genre: result });
return result.id;
} catch (error) {
Expand Down
2 changes: 2 additions & 0 deletions src/store/labels.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export default {
async index({ commit, rootState }) {
const generator = index(rootState.auth);
try {
await this.labelsRestored;
await fetchAll(commit, generator, "setLabels");
return true;
} catch (error) {
Expand All @@ -77,6 +78,7 @@ export default {
async read({ commit, rootState }, id) {
try {
const result = await read(rootState.auth, id);
await this.labelsRestored;
commit("setLabel", { id, label: result });
return result.id;
} catch (error) {
Expand Down
114 changes: 114 additions & 0 deletions src/store/persistence.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import VuexPersistence from "vuex-persist";
import localForage from "localforage";
import debounce from "lodash.debounce";

// A subclass for VuexPersistence to deal with single namespaced 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) {
const lowerCaseModule = module.toLowerCase();
// 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: [lowerCaseModule],
key: lowerCaseModule,
filter: (m) =>
m.type.substring(0, m.type.indexOf("/")) === lowerCaseModule,
saveState: debounce(saveModule, 60000),
});

this.capitilizedModule = module;

// Get item from an async storage and commit them back to the store
this.replaceState = async (
lowerCaseModule,
capitilizedModule,
storage,
store
) => {
const savedState = await storage.getItem(lowerCaseModule);
if (savedState) {
if (savedState.startLoading) {
store.commit(
`${lowerCaseModule}/setStartLoading`,
savedState.startLoading
);
}
if (savedState[lowerCaseModule]) {
store.commit(
`${lowerCaseModule}/set${capitilizedModule}`,
savedState[lowerCaseModule]
);
}
}
};

// Overwrite plugin to use replaceState and set a custom restored prop for each module
this.plugin = (store) => {
store[`${this.key}Restored`] = this.replaceState(
this.key,
this.capitilizedModule,
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",
"Plays",
"Tracks",
].map((module) => {
const plugin = new VuexPersistentModule(module, localForage);
return plugin.plugin;
});

const localStorageModules = [
"auth",
"codecConversions",
"codecs",
"coverFilenames",
"imageTypes",
"locations",
"rescan",
"users",
"userSettings",
];

export const vuexLocalStorage = new VuexPersistence({
storage: window.localStorage,
strictMode: process.env.NODE_ENV !== "production",
modules: localStorageModules,
filter: (m) =>
localStorageModules.includes(m.type.substring(0, m.type.indexOf("/"))),
});

plugins.push(vuexLocalStorage.plugin);

export default plugins;
1 change: 1 addition & 0 deletions src/store/plays.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export default {
async index({ commit, rootState }) {
const generator = index(rootState.auth);
try {
await this.playsRestored;
await fetchAll(commit, generator, "setPlays");
return true;
} catch (error) {
Expand Down
42 changes: 22 additions & 20 deletions src/store/store.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Vue from "vue";
import Vuex from "vuex";
import createPersistedState from "vuex-persistedstate";
import persistencePlugins, { vuexLocalStorage } from "./persistence";
import albums from "./albums";
import artists from "./artists";
import auth from "./auth";
Expand All @@ -20,12 +20,27 @@ import userSettings from "./user_settings";

Vue.use(Vuex);

const mutations = {
addError(state, error) {
if (error.unauthorized) {
this.commit("auth/logout");
}
state.errors.push(error);
},
clearErrors(state) {
state.errors = [];
},
updateCurrentDay(state) {
state.currentDay = new Date().setHours(0, 0, 0, 0);
},
};

if (process.env.NODE_ENV !== "production") {
mutations.RESTORE_MUTATION = vuexLocalStorage.RESTORE_MUTATION;
}

export default new Vuex.Store({
plugins: [
createPersistedState({
paths: ["auth", "userSettings"],
}),
],
plugins: persistencePlugins,
strict: process.env.NODE_ENV !== "production",
modules: {
albums,
Expand All @@ -49,20 +64,7 @@ export default new Vuex.Store({
errors: [],
currentDay: new Date().setHours(0, 0, 0, 0),
},
mutations: {
addError(state, error) {
if (error.unauthorized) {
this.commit("auth/logout");
}
state.errors.push(error);
},
clearErrors(state) {
state.errors = [];
},
updateCurrentDay(state) {
state.currentDay = new Date().setHours(0, 0, 0, 0);
},
},
mutations,
actions: {},
getters: {
numberOfFlaggedItems(state, getters) {
Expand Down
2 changes: 2 additions & 0 deletions src/store/tracks.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export default {
async index({ commit, rootState }, scope = new TracksScope()) {
const generator = index(rootState.auth, scope);
try {
await this.tracksRestored;
await fetchAll(commit, generator, "setTracks", scope);
return true;
} catch (error) {
Expand All @@ -121,6 +122,7 @@ export default {
async read({ commit, rootState }, id) {
try {
const track = await read(rootState.auth, id);
await this.tracksRestored;
commit("setTrack", { id, track });
return true;
} catch (error) {
Expand Down
39 changes: 29 additions & 10 deletions yarn.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.