252 lines
7 KiB
JavaScript
252 lines
7 KiB
JavaScript
import merge from 'lodash.merge'
|
|
import { each, get, set, cloneDeep } from 'lodash'
|
|
import { useInterfaceStore } from 'src/stores/interface'
|
|
import { storage } from './storage.js'
|
|
|
|
let loaded = false
|
|
|
|
const defaultReducer = (state, paths) => (
|
|
paths.length === 0
|
|
? state
|
|
: paths.reduce((substate, path) => {
|
|
set(substate, path, get(state, path))
|
|
return substate
|
|
}, {})
|
|
)
|
|
|
|
const saveImmedeatelyActions = [
|
|
'markNotificationsAsSeen',
|
|
'clearCurrentUser',
|
|
'setCurrentUser',
|
|
'setHighlight',
|
|
'setOption',
|
|
'setClientData',
|
|
'setToken',
|
|
'clearToken'
|
|
]
|
|
|
|
const defaultStorage = (() => {
|
|
return storage
|
|
})()
|
|
|
|
export default function createPersistedState ({
|
|
key = 'vuex-lz',
|
|
paths = [],
|
|
getState = (key, storage) => {
|
|
const value = storage.getItem(key)
|
|
return value
|
|
},
|
|
setState = (key, state, storage) => {
|
|
if (!loaded) {
|
|
console.info('waiting for old state to be loaded...')
|
|
return Promise.resolve()
|
|
} else {
|
|
return storage.setItem(key, state)
|
|
}
|
|
},
|
|
reducer = defaultReducer,
|
|
storage = defaultStorage,
|
|
subscriber = store => handler => store.subscribe(handler)
|
|
} = {}) {
|
|
return getState(key, storage).then((savedState) => {
|
|
return store => {
|
|
try {
|
|
if (savedState !== null && typeof savedState === 'object') {
|
|
// build user cache
|
|
const usersState = savedState.users || {}
|
|
usersState.usersObject = {}
|
|
const users = usersState.users || []
|
|
each(users, (user) => { usersState.usersObject[user.id] = user })
|
|
savedState.users = usersState
|
|
|
|
store.replaceState(
|
|
merge({}, store.state, savedState)
|
|
)
|
|
}
|
|
loaded = true
|
|
} catch (e) {
|
|
console.error("Couldn't load state")
|
|
console.error(e)
|
|
loaded = true
|
|
}
|
|
subscriber(store)((mutation, state) => {
|
|
try {
|
|
if (saveImmedeatelyActions.includes(mutation.type)) {
|
|
setState(key, reducer(cloneDeep(state), paths), storage)
|
|
.then(success => {
|
|
if (typeof success !== 'undefined') {
|
|
if (mutation.type === 'setOption' || mutation.type === 'setCurrentUser') {
|
|
useInterfaceStore().settingsSaved({ success })
|
|
}
|
|
}
|
|
}, error => {
|
|
if (mutation.type === 'setOption' || mutation.type === 'setCurrentUser') {
|
|
useInterfaceStore().settingsSaved({ error })
|
|
}
|
|
})
|
|
}
|
|
} catch (e) {
|
|
console.error("Couldn't persist state:")
|
|
console.error(e)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* This persists state for pinia, which falls back to read from the vuex state
|
|
* if pinia persisted state does not exist.
|
|
*
|
|
* When you migrate a module from vuex to pinia, you have to keep the original name.
|
|
* If the module was called `xxx`, the name of the store has to be `xxx` too.
|
|
*
|
|
* This adds one property to the store, $persistLoaded, which is a promise
|
|
* that resolves when the initial state is loaded. If the plugin is not enabled,
|
|
* $persistLoaded is a promise that resolves immediately.
|
|
* If we are not able to get the stored state because storage.getItem() throws or
|
|
* rejects, $persistLoaded will be a rejected promise with the thrown error.
|
|
*
|
|
* Call signature:
|
|
*
|
|
* defineStore(name, {
|
|
* ...,
|
|
* // setting the `persist` property enables this plugin
|
|
* // IMPORTANT: by default it is disabled, you have to set `persist` to at least an empty object
|
|
* persist: {
|
|
* // set to list of individual paths, or undefined/unset to persist everything
|
|
* paths: [],
|
|
* // function to call after loading initial state
|
|
* // if afterLoad is a function, it must return a state object that will be sent to `store.$patch`, or a promise to the state object
|
|
* // by default afterLoad is undefined
|
|
* afterLoad: (originalState) => {
|
|
* // ...
|
|
* return modifiedState
|
|
* },
|
|
* // if it exists, only persist state after these actions
|
|
* // if it doesn't exist or is undefined, persist state after every mutation of the state
|
|
* saveImmediatelyActions: [],
|
|
* // what to do after successfully saving the state
|
|
* onSaveSuccess: () => {},
|
|
* // what to do after there is an error saving the state
|
|
* onSaveError: () => {}
|
|
* }
|
|
* })
|
|
*
|
|
*/
|
|
export const piniaPersistPlugin = ({
|
|
vuexKey = 'vuex-lz',
|
|
keyFunction = (id) => `pinia-local-${id}`,
|
|
storage = defaultStorage,
|
|
reducer = defaultReducer
|
|
} = {}) => ({ store, options }) => {
|
|
if (!options.persist) {
|
|
return {
|
|
$persistLoaded: Promise.resolve()
|
|
}
|
|
}
|
|
|
|
let resolveLoaded
|
|
let rejectLoaded
|
|
const loadedPromise = new Promise((resolve, reject) => {
|
|
resolveLoaded = resolve
|
|
rejectLoaded = reject
|
|
})
|
|
|
|
const {
|
|
afterLoad,
|
|
paths = [],
|
|
saveImmediatelyActions,
|
|
onSaveSuccess = () => {},
|
|
onSaveError = () => {}
|
|
} = options.persist || {}
|
|
|
|
const loadedGuard = { loaded: false }
|
|
const key = keyFunction(store.$id)
|
|
const getState = async () => {
|
|
const id = store.$id
|
|
const value = await storage.getItem(key)
|
|
if (value) {
|
|
return value
|
|
}
|
|
|
|
const fallbackValue = await storage.getItem(vuexKey)
|
|
if (fallbackValue && fallbackValue[id]) {
|
|
console.info(`Migrating ${id} store data from vuex to pinia`)
|
|
const res = fallbackValue[id]
|
|
await storage.setItem(key, res)
|
|
return res
|
|
}
|
|
|
|
return {}
|
|
}
|
|
|
|
const setState = (state) => {
|
|
if (!loadedGuard.loaded) {
|
|
console.info('waiting for old state to be loaded...')
|
|
return Promise.reject()
|
|
} else {
|
|
return storage.setItem(key, state)
|
|
}
|
|
}
|
|
|
|
const getMaybeAugmentedState = async () => {
|
|
const savedRawState = await getState()
|
|
if (typeof afterLoad === 'function') {
|
|
try {
|
|
return await afterLoad(savedRawState)
|
|
} catch (e) {
|
|
console.error('Error running afterLoad:', e)
|
|
return savedRawState
|
|
}
|
|
} else {
|
|
return savedRawState
|
|
}
|
|
}
|
|
|
|
const persistCurrentState = async (state) => {
|
|
const stateClone = cloneDeep(state)
|
|
const stateToPersist = reducer(stateClone, paths)
|
|
try {
|
|
const res = await setState(stateToPersist)
|
|
onSaveSuccess(res)
|
|
} catch (e) {
|
|
console.error('Cannot persist state:', e)
|
|
onSaveError(e)
|
|
}
|
|
}
|
|
|
|
getMaybeAugmentedState()
|
|
.then(savedState => {
|
|
if (savedState) {
|
|
store.$patch(savedState)
|
|
}
|
|
|
|
loadedGuard.loaded = true
|
|
resolveLoaded()
|
|
|
|
// only subscribe after we have done setting the initial state
|
|
if (!saveImmediatelyActions) {
|
|
store.$subscribe(async (_mutation, state) => {
|
|
await persistCurrentState(state)
|
|
})
|
|
} else {
|
|
store.$onAction(({
|
|
name,
|
|
store,
|
|
after,
|
|
}) => {
|
|
if (saveImmediatelyActions.includes(name)) {
|
|
after(() => persistCurrentState(store.$state))
|
|
}
|
|
})
|
|
}
|
|
}, error => {
|
|
console.error('Cannot load storage:', error)
|
|
rejectLoaded(error)
|
|
})
|
|
|
|
return {
|
|
$persistLoaded: loadedPromise
|
|
}
|
|
}
|