@datorama/akita
Version:
A Reactive State Management Tailored-Made for JS Applications
184 lines • 7.17 kB
JavaScript
import { filter, from, isObservable, map, of, ReplaySubject, skip } from 'rxjs';
import { setAction } from './actions';
import { $$addStore, $$deleteStore } from './dispatchers';
import { getValue } from './getValueByString';
import { isFunction } from './isFunction';
import { isNil } from './isNil';
import { isObject } from './isObject';
import { hasLocalStorage, hasSessionStorage, isNotBrowser } from './root';
import { setValue } from './setValueByString';
import { __stores__ } from './stores';
let skipStorageUpdate = false;
const _persistStateInit = new ReplaySubject(1);
export function selectPersistStateInit() {
return _persistStateInit.asObservable();
}
export function setSkipStorageUpdate(skip) {
skipStorageUpdate = skip;
}
export function getSkipStorageUpdate() {
return skipStorageUpdate;
}
function isPromise(v) {
return v && isFunction(v.then);
}
function observify(asyncOrValue) {
if (isPromise(asyncOrValue) || isObservable(asyncOrValue)) {
return from(asyncOrValue);
}
return of(asyncOrValue);
}
export function persistState(params) {
const defaults = {
key: 'AkitaStores',
enableInNonBrowser: false,
storage: !hasLocalStorage() ? params.storage : localStorage,
deserialize: JSON.parse,
serialize: JSON.stringify,
include: [],
select: [],
persistOnDestroy: false,
preStorageUpdate: function (storeName, state) {
return state;
},
preStoreUpdate: function (storeName, state) {
return state;
},
skipStorageUpdate: getSkipStorageUpdate,
preStorageUpdateOperator: () => (source) => source,
};
const { storage, enableInNonBrowser, deserialize, serialize, include, select, key, preStorageUpdate, persistOnDestroy, preStorageUpdateOperator, preStoreUpdate, skipStorageUpdate } = Object.assign({}, defaults, params);
if ((isNotBrowser && !enableInNonBrowser) || !storage)
return;
const hasInclude = include.length > 0;
const hasSelect = select.length > 0;
let includeStores;
let selectStores;
if (hasInclude) {
includeStores = include.reduce((acc, path) => {
if (isFunction(path)) {
acc.fns.push(path);
}
else {
const storeName = path.split('.')[0];
acc[storeName] = path;
}
return acc;
}, { fns: [] });
}
if (hasSelect) {
selectStores = select.reduce((acc, selectFn) => {
acc[selectFn.storeName] = selectFn;
return acc;
}, {});
}
let stores = {};
const acc = {};
const subscriptions = [];
const buffer = [];
function _save(v) {
observify(v).subscribe(() => {
const next = buffer.shift();
next && _save(next);
});
}
// when we use the local/session storage we perform the serialize, otherwise we let the passed storage implementation to do it
const isLocalStorage = (hasLocalStorage() && storage === localStorage) || (hasSessionStorage() && storage === sessionStorage);
observify(storage.getItem(key)).subscribe((value) => {
let storageState = isObject(value) ? value : deserialize(value || '{}');
function save(storeCache) {
storageState['$cache'] = { ...(storageState['$cache'] || {}), ...storeCache };
storageState = Object.assign({}, storageState, acc);
buffer.push(storage.setItem(key, isLocalStorage ? serialize(storageState) : storageState));
_save(buffer.shift());
}
function subscribe(storeName, path) {
stores[storeName] = __stores__[storeName]
._select((state) => getValue(state, path))
.pipe(skip(1), map((store) => {
if (hasSelect && selectStores[storeName]) {
return selectStores[storeName](store);
}
return store;
}), filter(() => skipStorageUpdate() === false), preStorageUpdateOperator())
.subscribe((data) => {
acc[storeName] = preStorageUpdate(storeName, data);
Promise.resolve().then(() => save({ [storeName]: __stores__[storeName]._cache().getValue() }));
});
}
function setInitial(storeName, store, path) {
if (storeName in storageState) {
setAction('@PersistState');
store._setState((state) => {
return setValue(state, path, preStoreUpdate(storeName, storageState[storeName], state));
});
const hasCache = storageState['$cache'] ? storageState['$cache'][storeName] : false;
__stores__[storeName].setHasCache(hasCache, { restartTTL: true });
}
}
subscriptions.push($$deleteStore.subscribe((storeName) => {
if (stores[storeName]) {
if (persistOnDestroy === false) {
save({ [storeName]: false });
}
stores[storeName].unsubscribe();
delete stores[storeName];
}
}));
subscriptions.push($$addStore.subscribe((storeName) => {
if (storeName === 'router') {
return;
}
const store = __stores__[storeName];
if (hasInclude) {
let path = includeStores[storeName];
if (!path) {
const passPredicate = includeStores.fns.some((fn) => fn(storeName));
if (passPredicate) {
path = storeName;
}
else {
return;
}
}
setInitial(storeName, store, path);
subscribe(storeName, path);
}
else {
setInitial(storeName, store, storeName);
subscribe(storeName, storeName);
}
}));
_persistStateInit.next(true);
});
return {
destroy() {
subscriptions.forEach((s) => s.unsubscribe());
for (let i = 0, keys = Object.keys(stores); i < keys.length; i++) {
const storeName = keys[i];
stores[storeName].unsubscribe();
}
stores = {};
},
clear() {
storage.clear();
},
clearStore(storeName) {
if (isNil(storeName)) {
const value = observify(storage.setItem(key, '{}'));
value.subscribe();
return;
}
const value = storage.getItem(key);
observify(value).subscribe((v) => {
const storageState = deserialize(v || '{}');
if (storageState[storeName]) {
delete storageState[storeName];
const value = observify(storage.setItem(key, serialize(storageState)));
value.subscribe();
}
});
},
};
}
//# sourceMappingURL=persistState.js.map