@ngxs/storage-plugin
Version:
extendable storage plugin for @ngxs/store
305 lines (295 loc) • 15.8 kB
JavaScript
import * as i0 from '@angular/core';
import { inject, Injector, Injectable, PLATFORM_ID, NgModule, makeEnvironmentProviders, ENVIRONMENT_INITIALIZER, InjectionToken } from '@angular/core';
import { withNgxsPlugin } from '@ngxs/store';
import { ɵDEFAULT_STATE_KEY as _DEFAULT_STATE_KEY, ɵNGXS_STORAGE_PLUGIN_OPTIONS as _NGXS_STORAGE_PLUGIN_OPTIONS, ɵextractStringKey as _extractStringKey, ɵisKeyWithExplicitEngine as _isKeyWithExplicitEngine, STORAGE_ENGINE, ɵALL_STATES_PERSISTED as _ALL_STATES_PERSISTED, ɵUSER_OPTIONS as _USER_OPTIONS } from '@ngxs/storage-plugin/internals';
export { STORAGE_ENGINE } from '@ngxs/storage-plugin/internals';
import { isPlatformServer, isPlatformBrowser } from '@angular/common';
import { actionMatcher, InitState, UpdateState, getValue, setValue } from '@ngxs/store/plugins';
import { tap } from 'rxjs/operators';
function storageOptionsFactory(options) {
return {
storage: 0 /* StorageOption.LocalStorage */,
serialize: JSON.stringify,
deserialize: JSON.parse,
beforeSerialize: obj => obj,
afterDeserialize: obj => obj,
...options,
keys: options.keys === '*' ? [_DEFAULT_STATE_KEY] : options.keys
};
}
function engineFactory(options, platformId) {
if (isPlatformServer(platformId)) {
return null;
}
if (options.storage === 0 /* StorageOption.LocalStorage */) {
return localStorage;
}
else if (options.storage === 1 /* StorageOption.SessionStorage */) {
return sessionStorage;
}
return null;
}
function getStorageKey(key, options) {
// Prepends the `namespace` option to any key if it's been provided by a user.
// So `@@STATE` becomes `my-app:@@STATE`.
return options?.namespace ? `${options.namespace}:${key}` : key;
}
class ɵNgxsStoragePluginKeysManager {
constructor() {
/** Store keys separately in a set so we're able to check if the key already exists. */
this._keys = new Set();
this._injector = inject(Injector);
this._keysWithEngines = [];
const { keys } = inject(_NGXS_STORAGE_PLUGIN_OPTIONS);
this.addKeys(keys);
}
getKeysWithEngines() {
// Spread to prevent external code from directly modifying the internal state.
return [...this._keysWithEngines];
}
addKeys(storageKeys) {
for (const storageKey of storageKeys) {
const key = _extractStringKey(storageKey);
// The user may call `withStorageFeature` with the same state multiple times.
// Let's prevent duplicating state names in the `keysWithEngines` list.
// Please note that calling provideStates multiple times with the same state is
// acceptable behavior. This may occur because the state could be necessary at the
// feature level, and different parts of the application might require its registration.
// Consequently, `withStorageFeature` may also be called multiple times.
if (this._keys.has(key)) {
continue;
}
this._keys.add(key);
const engine = _isKeyWithExplicitEngine(storageKey)
? this._injector.get(storageKey.engine)
: this._injector.get(STORAGE_ENGINE);
this._keysWithEngines.push({ key, engine });
}
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: ɵNgxsStoragePluginKeysManager, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: ɵNgxsStoragePluginKeysManager, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: ɵNgxsStoragePluginKeysManager, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [] });
class NgxsStoragePlugin {
constructor() {
this._keysManager = inject(ɵNgxsStoragePluginKeysManager);
this._options = inject(_NGXS_STORAGE_PLUGIN_OPTIONS);
this._allStatesPersisted = inject(_ALL_STATES_PERSISTED);
this._isServer = isPlatformServer(inject(PLATFORM_ID));
}
handle(state, event, next) {
if (this._isServer) {
return next(state, event);
}
const matches = actionMatcher(event);
const isInitAction = matches(InitState);
const isUpdateAction = matches(UpdateState);
const isInitOrUpdateAction = isInitAction || isUpdateAction;
let hasMigration = false;
if (isInitOrUpdateAction) {
const addedStates = isUpdateAction && event.addedStates;
for (const { key, engine } of this._keysManager.getKeysWithEngines()) {
// We're checking what states have been added by NGXS and if any of these states should be handled by
// the storage plugin. For instance, we only want to deserialize the `auth` state, NGXS has added
// the `user` state, the storage plugin will be rerun and will do redundant deserialization.
// `usesDefaultStateKey` is necessary to check since `event.addedStates` never contains `@@STATE`.
if (!this._allStatesPersisted && addedStates) {
// We support providing keys that can be deeply nested via dot notation, for instance,
// `keys: ['myState.myProperty']` is a valid key.
// The state name should always go first. The below code checks if the `key` includes dot
// notation and extracts the state name out of the key.
// Given the `key` is `myState.myProperty`, the `addedStates` will only contain `myState`.
const dotNotationIndex = key.indexOf(DOT);
const stateName = dotNotationIndex > -1 ? key.slice(0, dotNotationIndex) : key;
if (!addedStates.hasOwnProperty(stateName)) {
continue;
}
}
const storageKey = getStorageKey(key, this._options);
let storedValue = engine.getItem(storageKey);
if (storedValue !== 'undefined' && storedValue != null) {
try {
const newVal = this._options.deserialize(storedValue);
storedValue = this._options.afterDeserialize(newVal, key);
}
catch {
typeof ngDevMode !== 'undefined' &&
ngDevMode &&
console.error(`Error ocurred while deserializing the ${storageKey} store value, falling back to empty object, the value obtained from the store: `, storedValue);
storedValue = {};
}
this._options.migrations?.forEach(strategy => {
const versionMatch = strategy.version === getValue(storedValue, strategy.versionKey || 'version');
const keyMatch = (!strategy.key && this._allStatesPersisted) || strategy.key === key;
if (versionMatch && keyMatch) {
storedValue = strategy.migrate(storedValue);
hasMigration = true;
}
});
if (this._allStatesPersisted) {
storedValue = this._hydrateSelectivelyOnUpdate(storedValue, addedStates);
state = { ...state, ...storedValue };
}
else {
state = setValue(state, key, storedValue);
}
}
}
}
return next(state, event).pipe(tap(nextState => {
if (isInitOrUpdateAction && !hasMigration) {
return;
}
for (const { key, engine } of this._keysManager.getKeysWithEngines()) {
let storedValue = nextState;
const storageKey = getStorageKey(key, this._options);
if (key !== _DEFAULT_STATE_KEY) {
storedValue = getValue(nextState, key);
}
try {
const newStoredValue = this._options.beforeSerialize(storedValue, key);
engine.setItem(storageKey, this._options.serialize(newStoredValue));
}
catch (error) {
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
if (error &&
(error.name === 'QuotaExceededError' ||
error.name === 'NS_ERROR_DOM_QUOTA_REACHED')) {
console.error(`The ${storageKey} store value exceeds the browser storage quota: `, storedValue);
}
else {
console.error(`Error ocurred while serializing the ${storageKey} store value, value not updated, the value obtained from the store: `, storedValue);
}
}
}
}
}));
}
_hydrateSelectivelyOnUpdate(storedValue, addedStates) {
// The `UpdateState` action is triggered whenever a feature state is added.
// The condition below is only satisfied when this action is triggered.
// Let's consider two states: `counter` and `@ngxs/router-plugin` state.
// When `provideStore` is called, `CounterState` is provided at the root level,
// while `@ngxs/router-plugin` is provided as a feature state. Previously, the storage
// plugin might have stored the value of the counter state as `10`. If `CounterState`
// implements the `ngxsOnInit` hook and sets the state to `999`, the storage plugin will
// reset the entire state when the `RouterState` is registered.
// Consequently, the `counter` state will revert back to `10` instead of `999`.
if (!storedValue || !addedStates || Object.keys(addedStates).length === 0) {
// Nothing to update if `addedStates` object is empty.
return storedValue;
}
// The `storedValue` can be the entire state when the default state key
// is used. However, if `addedStates` only contains the `router` value,
// we only want to merge the state with that `router` value.
// Given the `storedValue` is an object:
// `{ counter: 10, router: {...} }`
// This will only select the `router` object from the `storedValue`,
// avoiding unnecessary rehydration of the `counter` state.
return Object.keys(addedStates).reduce((accumulator, addedState) => {
if (storedValue.hasOwnProperty(addedState)) {
accumulator[addedState] = storedValue[addedState];
}
return accumulator;
}, {});
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: NgxsStoragePlugin, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: NgxsStoragePlugin }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: NgxsStoragePlugin, decorators: [{
type: Injectable
}] });
const DOT = '.';
class NgxsStoragePluginModule {
static forRoot(options) {
return {
ngModule: NgxsStoragePluginModule,
providers: [
withNgxsPlugin(NgxsStoragePlugin),
{
provide: _USER_OPTIONS,
useValue: options
},
{
provide: _NGXS_STORAGE_PLUGIN_OPTIONS,
useFactory: storageOptionsFactory,
deps: [_USER_OPTIONS]
},
{
provide: STORAGE_ENGINE,
useFactory: engineFactory,
deps: [_NGXS_STORAGE_PLUGIN_OPTIONS, PLATFORM_ID]
}
]
};
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: NgxsStoragePluginModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
/** @nocollapse */ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.0.0", ngImport: i0, type: NgxsStoragePluginModule }); }
/** @nocollapse */ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: NgxsStoragePluginModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: NgxsStoragePluginModule, decorators: [{
type: NgModule
}] });
function withNgxsStoragePlugin(options) {
return makeEnvironmentProviders([
withNgxsPlugin(NgxsStoragePlugin),
{
provide: _USER_OPTIONS,
useValue: options
},
{
provide: _NGXS_STORAGE_PLUGIN_OPTIONS,
useFactory: storageOptionsFactory,
deps: [_USER_OPTIONS]
},
{
provide: STORAGE_ENGINE,
useFactory: engineFactory,
deps: [_NGXS_STORAGE_PLUGIN_OPTIONS, PLATFORM_ID]
}
]);
}
function withStorageFeature(storageKeys) {
return makeEnvironmentProviders([
{
provide: ENVIRONMENT_INITIALIZER,
multi: true,
useValue: () => {
const allStatesPersisted = inject(_ALL_STATES_PERSISTED);
if (allStatesPersisted) {
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
const message = 'The NGXS storage plugin is currently persisting all states because the `keys` ' +
'option was explicitly set to `*` at the root level. To selectively persist states, ' +
'consider explicitly specifying them, allowing for addition at the feature level.';
console.error(message);
}
// We should prevent the addition of any feature states to persistence
// if the `keys` property is set to `*`, as this could disrupt the algorithm
// used in the storage plugin. Instead, we should log an error in development
// mode. In production, it should continue to function, but act as a no-op.
return;
}
inject(ɵNgxsStoragePluginKeysManager).addKeys(storageKeys);
}
}
]);
}
const LOCAL_STORAGE_ENGINE = /* @__PURE__ */ new InjectionToken(typeof ngDevMode !== 'undefined' && ngDevMode ? 'LOCAL_STORAGE_ENGINE' : '', {
providedIn: 'root',
factory: () => (isPlatformBrowser(inject(PLATFORM_ID)) ? localStorage : null)
});
const SESSION_STORAGE_ENGINE = /* @__PURE__ */ new InjectionToken(typeof ngDevMode !== 'undefined' && ngDevMode ? 'SESSION_STORAGE_ENGINE' : '', {
providedIn: 'root',
factory: () => (isPlatformBrowser(inject(PLATFORM_ID)) ? sessionStorage : null)
});
/**
* The public api for consumers of @ngxs/storage-plugin
*/
/**
* Generated bundle index. Do not edit.
*/
export { LOCAL_STORAGE_ENGINE, NgxsStoragePlugin, NgxsStoragePluginModule, SESSION_STORAGE_ENGINE, withNgxsStoragePlugin, withStorageFeature };
//# sourceMappingURL=ngxs-storage-plugin.mjs.map