pinia-plugin-state-persistence
Version:
Pinia plugin for universal state persistence across synchronous and asynchronous storage systems, supporting advanced features like path inclusion/exclusion and custom serialization.
122 lines (121 loc) • 6.02 kB
JavaScript
import { applyStateFilter, createLogger, getObjectDiff, isPromise, queueTask } from './utils.js';
export function createStatePersistence(globalOptions = {}) {
const queues = {};
const detectStorage = (log) => {
if (typeof window === 'undefined') {
log.info('SSR detected, no storage available.');
return null;
}
if (window.localStorage) {
log.info('Using localStorage.');
return window.localStorage;
}
log.error('No valid storage found, persistence disabled.');
return null;
};
return (context) => {
const storeOptions = context.options.persist;
if (!storeOptions) {
return;
}
const persistOptionsArray = Array.isArray(storeOptions)
? storeOptions
: storeOptions === true
? [{}]
: [storeOptions];
persistOptionsArray.forEach((options) => {
let { key = context.store.$id, debug = false, overwrite = false, storage = null, filter = () => true, serialize = JSON.stringify, deserialize = JSON.parse, deepCopy = false, clientOnly = false, include = null, exclude = null, } = { ...{ ...globalOptions, key: undefined }, ...options };
const log = createLogger(debug);
if (!storage)
storage = detectStorage(log);
if (!storage || ((clientOnly || storage.constructor.name.includes('LocalForage')) && typeof window === 'undefined')) {
log.warn(`Skipping ${context.store.$id}, storage unavailable.`);
return;
}
const getPrefixedKey = (storeKey) => globalOptions.key ? `${globalOptions.key}:${storeKey}` : storeKey;
const loadState = () => {
const tasks = [];
let stateToRestore = {};
const getItem = (key) => {
try {
const result = storage.getItem(getPrefixedKey(key));
if (isPromise(result)) {
const task = queueTask(queues, key, async () => await result);
tasks.push(task);
return task;
}
return result;
}
catch (error) {
log.error(`Error retrieving ${key}:`, error);
}
};
const restoreState = (state) => {
if (!state || Object.keys(state).length === 0) {
log.warn(`No state to restore for ${context.store.$id}.`);
return;
}
log.info(`Restoring state for ${context.store.$id}`);
overwrite ? (context.store.$state = state) : context.store.$patch(state);
};
const resolveAndDeserialize = (storageKey, stateKey) => {
const processValue = (value) => {
if (value) {
const deserializedValue = typeof value === 'object' ? value : deserialize(value);
stateKey ? (stateToRestore[stateKey] = deserializedValue) : (stateToRestore = deserializedValue);
}
};
const savedValue = getItem(storageKey);
isPromise(savedValue) ? savedValue.then(processValue) : processValue(savedValue);
};
resolveAndDeserialize(typeof key === 'string' ? key : context.store.$id);
if (typeof key === 'object') {
Object.entries(key).forEach(([stateKey, storageKey]) => resolveAndDeserialize(storageKey, stateKey));
}
if (tasks.length) {
return Promise.all(tasks).then(() => restoreState(stateToRestore));
}
restoreState(stateToRestore);
};
const persistState = (mutation, state) => {
if (!filter(mutation, state)) {
log.info(`Skipping persistence for ${context.store.$id}.`);
return;
}
const tasks = [];
const filteredState = applyStateFilter(state, include, exclude);
const setItem = (key, value) => {
try {
const result = storage.setItem(getPrefixedKey(key), deepCopy ? deserialize(value) : value);
if (isPromise(result)) {
tasks.push(queueTask(queues, key, async () => await result));
}
}
catch (error) {
log.error(`Failed to persist ${key}:`, error);
}
};
if (typeof key === 'string') {
setItem(key, serialize(filteredState));
}
else {
setItem(context.store.$id, serialize(getObjectDiff(filteredState, key)));
for (const [stateKey, storageKey] of Object.entries(key)) {
if (filteredState[stateKey] !== undefined)
setItem(storageKey, serialize(filteredState[stateKey]));
}
}
if (tasks.length) {
return Promise.all(tasks).then(() => {
log.info(`State persistence complete for ${context.store.$id}`);
});
}
log.info(`State persistence complete for ${context.store.$id}`);
};
context.store.$restore = loadState;
context.store.$persist = () => persistState({ type: 'persist', storeId: context.store.$id }, context.store.$state);
loadState();
context.store.$subscribe(persistState, { flush: 'sync' });
});
};
}