UNPKG

mini-rx-store

Version:

MiniRx: The Lightweight RxJS Redux Store

428 lines (412 loc) 14.7 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var rxjs = require('rxjs'); var common = require('@mini-rx/common'); function createSelectFn(state$) { function select(mapFnOrKey) { if (!mapFnOrKey) { return state$; } return state$.pipe(rxjs.map(state => { return common.isKey(state, mapFnOrKey) ? state[mapFnOrKey] : mapFnOrKey(state); }), rxjs.distinctUntilChanged()); } return select; } function createState(initialState) { const stateSource = new rxjs.BehaviorSubject(initialState); const state$ = stateSource.asObservable(); function get() { return stateSource.value; } function set(v) { stateSource.next(v); } return { select: createSelectFn(state$), get, set }; } function createLazyState(initialState) { const stateSource = new rxjs.BehaviorSubject(initialState); const state$ = stateSource.asObservable().pipe( // Skip the first (undefined) value of the BehaviorSubject // Very similar to a ReplaySubject(1), but more lightweight // Emits a state object (when calling the `set` method) rxjs.filter(v => !!v)); function get() { return stateSource.value; } function set(v) { stateSource.next(v); } return { select: createSelectFn(state$), get, set }; } const storeCore = common.createStore(createState({})); const actions$ = storeCore.actions$; const rxEffect = common.createRegisterEffectFn(storeCore.dispatch); class Store {} let isStoreConfigured = false; function configureStore(config) { if (!isStoreConfigured) { storeCore.configureStore(config); isStoreConfigured = true; return { feature: storeCore.addFeature, select: storeCore.appState.select, dispatch: storeCore.dispatch, effect: rxEffect }; } common.miniRxError('`configureStore` was called multiple times.'); } function createEffectFn(subSink) { function effect(effectFn) { const subject = new rxjs.Subject(); const effect$ = effectFn(subject); const effectWithDefaultErrorHandler = common.defaultEffectsErrorHandler(effect$); subSink.sink = effectWithDefaultErrorHandler.subscribe(); return observableOrValue => { rxjs.isObservable(observableOrValue) ? subSink.sink = observableOrValue.subscribe(v => subject.next(v)) : subject.next(observableOrValue); }; } return effect; } function createAssertState(constructorName, state) { function isInitialized() { const notInitializedErrorMessage = `${constructorName} has no initialState yet. ` + `Please provide an initialState before updating/getting state.`; if (!state.get()) { common.miniRxError(notInitializedErrorMessage); } } function isNotInitialized() { const initializedErrorMessage = `${constructorName} has initialState already.`; if (state.get()) { common.miniRxError(initializedErrorMessage); } } return { isInitialized, isNotInitialized }; } function createConnectFn(updateStateCallback, subSink) { return dict => { const keys = Object.keys(dict); keys.forEach(key => { const obs$ = dict[key]; subSink.sink = obs$.subscribe(v => { updateStateCallback({ [key]: v }, "connection" /* OperationType.CONNECTION */, key); }); }); }; } class FeatureStore { get featureKey() { return this._featureKey; } get state() { this.assertState.isInitialized(); return this._state.get(); } constructor(featureKey, initialState, config = {}) { this._state = createLazyState(); this.updateState = (stateOrCallback, operationType, name) => { this.assertState.isInitialized(); return storeCore.dispatch({ type: common.createMiniRxActionType(operationType, this.featureKey, name), stateOrCallback, featureId: this.featureId }); }; this.subSink = common.createSubSink(); this.assertState = createAssertState(this.constructor.name, this._state); this.setState = common.createUpdateFn(this.updateState); this.connect = createConnectFn(this.updateState, this.subSink); this.effect = createEffectFn(this.subSink); this.select = this._state.select; this.featureId = common.generateId(); this._featureKey = common.generateFeatureKey(featureKey, config.multi); if (initialState) { this.setInitialState(initialState); } } setInitialState(initialState) { this.assertState.isNotInitialized(); storeCore.addFeature(this._featureKey, common.createFeatureStoreReducer(this.featureId, initialState)); this.subSink.sink = storeCore.appState.select(state => state[this.featureKey]).subscribe(v => this._state.set(v)); } undo(action) { storeCore.hasUndoExtension ? storeCore.dispatch(common.undo(action)) : common.miniRxError('UndoExtension is not initialized.'); } destroy() { this.subSink.unsubscribe(); storeCore.removeFeature(this._featureKey); } /** * @internal * Can be called by Angular if ComponentStore/FeatureStore is provided in a component */ ngOnDestroy() { this.destroy(); } } function createFeatureStore(featureKey, initialState, config = {}) { return new FeatureStore(featureKey, initialState, config); } const globalCsConfig = common.componentStoreConfig(); function configureComponentStores(config) { globalCsConfig.set(config); } class ComponentStore { get state() { this.assertState.isInitialized(); return this._state.get(); } constructor(initialState, config) { this.config = config; this.extensions = common.calculateExtensions(this.config, globalCsConfig.get()); this.hasUndoExtension = this.extensions.some(ext => ext.id === 1 /* ExtensionId.UNDO */); this.actionsOnQueue = common.createActionsOnQueue(); this._state = createLazyState(); this.updateState = (stateOrCallback, operationType, name) => { this.assertState.isInitialized(); return this.actionsOnQueue.dispatch({ type: common.createMiniRxActionType(operationType, common.componentStoreFeatureKey, name), stateOrCallback }); }; this.subSink = common.createSubSink(); this.assertState = createAssertState(this.constructor.name, this._state); this.setState = common.createUpdateFn(this.updateState); this.connect = createConnectFn(this.updateState, this.subSink); this.effect = createEffectFn(this.subSink); this.select = this._state.select; if (initialState) { this.setInitialState(initialState); } } setInitialState(initialState) { this.assertState.isNotInitialized(); const reducer = common.createComponentStoreReducer(initialState, this.extensions); this.subSink.sink = this.actionsOnQueue.actions$.subscribe(action => { const newState = reducer(this._state.get(), // Initially undefined, but the reducer can handle undefined (by falling back to initial state) action); this._state.set(newState); }); this.actionsOnQueue.dispatch({ type: common.createMiniRxActionType("init" /* OperationType.INIT */, common.componentStoreFeatureKey) }); } undo(action) { this.hasUndoExtension ? this.actionsOnQueue.dispatch(common.undo(action)) : common.miniRxError(`${this.constructor.name} has no UndoExtension yet.`); } destroy() { if (this._state.get()) { // Dispatch an action really just for logging via LoggerExtension // Only dispatch if an initial state was provided or setInitialState was called this.actionsOnQueue.dispatch({ type: common.createMiniRxActionType("destroy" /* OperationType.DESTROY */, common.componentStoreFeatureKey) }); } this.subSink.unsubscribe(); } /** * @internal * Can be called by Angular if ComponentStore/FeatureStore is provided in a component */ ngOnDestroy() { this.destroy(); } } function createComponentStore(initialState, config) { return new ComponentStore(initialState, config); } function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); } // Credits go to Alexander Reardon // Copied from with small modifications: https://github.com/alexreardon/memoize-one/tree/v6.0.0/src // MIT License // // Copyright (c) 2019 Alexander Reardon // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. function isEqual(first, second) { if (first === second) { return true; } // Special case for NaN (NaN !== NaN) if (Number.isNaN(first) && Number.isNaN(second)) { return true; } return false; } function areInputsEqual(newInputs, lastInputs) { // no checks needed if the inputs length has changed if (newInputs.length !== lastInputs.length) { return false; } // Using for loop for speed. It generally performs better than array.every // https://github.com/alexreardon/memoize-one/pull/59 for (let i = 0; i < newInputs.length; i++) { if (!isEqual(newInputs[i], lastInputs[i])) { return false; } } return true; } function memoizeOne(resultFn) { let cache = null; // breaking cache when context (this) or arguments change function memoized(...newArgs) { if (cache && cache.lastThis === this && areInputsEqual(newArgs, cache.lastArgs)) { return cache.lastResult; } // Throwing during an assignment aborts the assignment: https://codepen.io/alexreardon/pen/RYKoaz // Doing the lastResult assignment first so that if it throws // the cache will not be overwritten const lastResult = resultFn.apply(this, newArgs); cache = { lastResult, lastArgs: newArgs, lastThis: this }; return lastResult; } return memoized; } function createSelector(...args) { if (args.length === 1 && isSelectorsDictionary(args[0])) { args = extractArgsFromSelectorsDictionary(args[0]); } const selectors = args.slice(0, args.length - 1); const projector = args[args.length - 1]; const memoizedProjector = memoizeOne(projector); return memoizeOne(state => { const selectorResults = selectors.map(fn => fn(state)); return memoizedProjector(...selectorResults); }); } function createFeatureStateSelector(featureKey) { if (featureKey) { return createSelector(state => state[featureKey], featureState => featureState); } return state => state; // Do not memoize: when used with FeatureStore there is a new state object created for every `setState` } /** @deprecated Use `createFeatureStateSelector` which is more in line with `createComponentStateSelector` */ function createFeatureSelector(featureKey) { return createFeatureStateSelector(featureKey); } function createComponentStateSelector() { return state => state; } function isSelectorsDictionary(selectors) { return !!selectors && typeof selectors === 'object' && Object.values(selectors).every(selector => typeof selector === 'function'); } function extractArgsFromSelectorsDictionary(selectorsDictionary) { const selectors = Object.values(selectorsDictionary); const resultKeys = Object.keys(selectorsDictionary); const projector = (...selectorResults) => resultKeys.reduce((result, key, index) => _extends({}, result, { [key]: selectorResults[index] }), {}); return [...selectors, projector]; } class ReduxDevtoolsExtension extends common.AbstractReduxDevtoolsExtension { get actions$() { return actions$; } readState() { return storeCore.appState.get(); } updateState(state) { storeCore.appState.set(state); } } Object.defineProperty(exports, 'Actions', { enumerable: true, get: function () { return common.Actions; } }); Object.defineProperty(exports, 'ImmutableStateExtension', { enumerable: true, get: function () { return common.ImmutableStateExtension; } }); Object.defineProperty(exports, 'LoggerExtension', { enumerable: true, get: function () { return common.LoggerExtension; } }); Object.defineProperty(exports, 'StoreExtension', { enumerable: true, get: function () { return common.StoreExtension; } }); Object.defineProperty(exports, 'UndoExtension', { enumerable: true, get: function () { return common.UndoExtension; } }); Object.defineProperty(exports, 'createEffect', { enumerable: true, get: function () { return common.createRxEffect; } }); Object.defineProperty(exports, 'hasEffectMetaData', { enumerable: true, get: function () { return common.hasEffectMetaData; } }); Object.defineProperty(exports, 'mapResponse', { enumerable: true, get: function () { return common.mapResponse; } }); Object.defineProperty(exports, 'ofType', { enumerable: true, get: function () { return common.ofType; } }); Object.defineProperty(exports, 'tapResponse', { enumerable: true, get: function () { return common.tapResponse; } }); Object.defineProperty(exports, 'undo', { enumerable: true, get: function () { return common.undo; } }); exports.ComponentStore = ComponentStore; exports.FeatureStore = FeatureStore; exports.ReduxDevtoolsExtension = ReduxDevtoolsExtension; exports.Store = Store; exports.actions$ = actions$; exports.configureComponentStores = configureComponentStores; exports.configureStore = configureStore; exports.createComponentStateSelector = createComponentStateSelector; exports.createComponentStore = createComponentStore; exports.createFeatureSelector = createFeatureSelector; exports.createFeatureStateSelector = createFeatureStateSelector; exports.createFeatureStore = createFeatureStore; exports.createSelector = createSelector;