UNPKG

@ngxs/storage-plugin

Version:

extendable storage plugin for @ngxs/store

305 lines (295 loc) 15.8 kB
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