UNPKG

reactive-state

Version:

Redux-like state management using RxJS and TypeScript

261 lines 14.3 kB
"use strict"; var __spreadArrays = (this && this.__spreadArrays) || function () { for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; for (var r = Array(s), k = 0, i = 0; i < il; i++) for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) r[k] = a[j]; return r; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Store = void 0; var rxjs_1 = require("rxjs"); var operators_1 = require("rxjs/operators"); var shallowEqual_1 = require("./shallowEqual"); var isPlainObject = require("lodash.isplainobject"); var isObject = require("lodash.isobject"); /** * Creates a state based on a stream of StateMutation functions and an initial state. The returned observable * is hot and caches the last emitted value (will emit the last emitted value immediately upon subscription). * @param stateMutators * @param initialState */ function createState(stateMutators, initialState) { var state = new rxjs_1.BehaviorSubject(initialState); stateMutators.pipe(operators_1.scan(function (state, reducer) { return reducer(state); }, initialState)).subscribe(state); return state; } var Store = /** @class */ (function () { function Store(state, stateMutators, forwardProjections, backwardProjections, notifyRootStateChangedSubject, actionDispatch) { /** * Is completed when the slice is unsubscribed and no longer needed. */ this._destroyed = new rxjs_1.AsyncSubject(); this.state = state; this.stateMutators = stateMutators; this.forwardProjections = forwardProjections; this.backwardProjections = backwardProjections; this.destroyed = this._destroyed.asObservable(); this.actionDispatch = actionDispatch; this.stateChangeNotificationSubject = notifyRootStateChangedSubject; this.stateChangedNotification = this.stateChangeNotificationSubject .asObservable() .pipe(operators_1.takeUntil(this.destroyed)); } Object.defineProperty(Store.prototype, "currentState", { get: function () { // TODO see above: this.state is actually a BehaviorSubject but typescript or rxjs typings make trouble return this.state.value; }, enumerable: false, configurable: true }); /** * Create a new Store based on an initial state */ Store.create = function (initialState) { if (initialState === undefined) initialState = {}; else { if (isObject(initialState) && !Array.isArray(initialState) && !isPlainObject(initialState)) throw new Error("initialState must be a plain object, an array, or a primitive type"); } var stateMutators = new rxjs_1.Subject(); var state = createState(stateMutators, initialState); var store = new Store(state, stateMutators, [], [], new rxjs_1.Subject(), new rxjs_1.Subject()); // emit a single state mutation so that we emit the initial state on subscription stateMutators.next(function (s) { return s; }); return store; }; /** * Creates a new linked store, that Selects a slice on the main store. * @deprecated */ Store.prototype.createSlice = function (key, initialState, cleanupState) { if (isObject(initialState) && !Array.isArray(initialState) && !isPlainObject(initialState)) throw new Error("initialState must be a plain object, an array, or a primitive type"); if (isObject(cleanupState) && !Array.isArray(cleanupState) && !isPlainObject(cleanupState)) throw new Error("cleanupState must be a plain object, an array, or a primitive type"); var forward = function (state) { return state[key]; }; var backward = function (state, parentState) { parentState[key] = state; return parentState; }; var initial = initialState === undefined ? undefined : function () { return initialState; }; // legacy cleanup for slices var cleanup = cleanupState === undefined ? undefined : function (state, parentState) { if (cleanupState === "undefined") { parentState[key] = undefined; } else if (cleanupState === "delete") delete parentState[key]; else { parentState[key] = cleanupState; } return parentState; }; return this.createProjection(forward, backward, initial, cleanup); }; /** * Create a clone of the store which holds the same state. This is an alias to createProjection with * the identity functions as forward/backwards projection. Usefull to unsubscribe from select()/watch() * subscriptions as the destroy() event is specific to the new cloned instance (=will not destroy the original) * Also usefull to scope string-based action dispatches to .dispatch() as action/reducers pairs added to the * clone can not be dispatched by the original and vice versa. */ Store.prototype.clone = function () { return this.createProjection(function (s) { return s; }, function (s, p) { return s; }); }; /** * Creates a new slice of the store. The slice holds a transformed state that is created by applying the * forwardProjection function. To transform the slice state back to the parent state, a backward projection * function must be given. * @param forwardProjection - Projection function that transforms a State S to a new projected state TProjectedState * @param backwardProjection - Back-Projection to obtain state S from already projected state TProjectedState * @param initial - Function to be called initially with state S that must return an initial state to use for TProjected * @param cleanup - Function to be called when the store is destroyed to return a cleanup state based on parent state S */ Store.prototype.createProjection = function (forwardProjection, backwardProjection, // TODO make this a flat object instead of a function? initial, cleanup) { var _this = this; var forwardProjections = __spreadArrays(this.forwardProjections, [forwardProjection]); var backwardProjections = __spreadArrays([backwardProjection], this.backwardProjections); var initialState = initial ? initial(this.state.value) : forwardProjection(this.state.value); if (initial !== undefined) { this.stateMutators.next(function (s) { var initialReducer = function () { return initialState; }; return mutateRootState(s, forwardProjections, backwardProjections, initialReducer); }); } var state = new rxjs_1.BehaviorSubject(initialState); this.state.pipe(operators_1.map(function (state) { return forwardProjection(state); })).subscribe(state); var onDestroy = function () { if (cleanup !== undefined) { _this.stateMutators.next(function (s) { var backward = __spreadArrays([cleanup], _this.backwardProjections); return mutateRootState(s, forwardProjections, backward, function (s) { return s; }); }); } }; var sliceStore = new Store(state, this.stateMutators, forwardProjections, backwardProjections, this.stateChangeNotificationSubject, this.actionDispatch); sliceStore.destroyed.subscribe(undefined, undefined, onDestroy); // destroy the slice if the parent gets destroyed this._destroyed.subscribe(undefined, undefined, function () { sliceStore.destroy(); }); return sliceStore; }; /** * Adds an Action/Reducer pair. This will make the reducer become active whenever the action observable emits a * value. * @param action An observable whose payload will be passed to the reducer on each emit, or a string identifier * as an action name. In the later case, .dispatch() can be used to manually dispatch actions based * on their string name. * @param reducer A reducer function. @see Reducer * @param actionName An optional name (only used during development/debugging) to assign to the action when an * Observable is passed as first argument. Must not be specified if the action argument is a string. */ Store.prototype.addReducer = function (action, reducer, actionName) { var _this = this; if (typeof action === "string" && typeof actionName !== "undefined") throw new Error("cannot specify a string-action AND a string alias at the same time"); if (!rxjs_1.isObservable(action) && typeof action !== "string") throw new Error("first argument must be an observable or a string"); if (typeof reducer !== "function") throw new Error("reducer argument must be a function"); if ((typeof actionName === "string" && actionName.length === 0) || (typeof action === "string" && action.length === 0)) throw new Error("action/actionName must have non-zero length"); var name = typeof action === "string" ? action : actionName; var actionFromStringBasedDispatch = this.actionDispatch.pipe(operators_1.filter(function (s) { return s.actionName === name; }), operators_1.map(function (s) { return s.actionPayload; }), operators_1.merge(rxjs_1.isObservable(action) ? action : rxjs_1.EMPTY), operators_1.takeUntil(this.destroyed)); var rootReducer = function (payload) { return function (rootState) { // transform the rootstate to a slice by applying all forward projections var sliceReducer = function (slice) { return reducer(slice, payload); }; rootState = mutateRootState(rootState, _this.forwardProjections, _this.backwardProjections, sliceReducer); // Send state change notification var changeNotification = { actionName: name, actionPayload: payload, newState: rootState, }; _this.stateChangeNotificationSubject.next(changeNotification); return rootState; }; }; return actionFromStringBasedDispatch.pipe(operators_1.map(function (payload) { return rootReducer(payload); })).subscribe(function (rootStateMutation) { _this.stateMutators.next(rootStateMutation); }); }; /** * Selects a part of the state using a selector function. If no selector function is given, the identity function * is used (which returns the state of type S). * Note: The returned observable does only update when the result of the selector function changed * compared to a previous emit. A shallow copy test is performed to detect changes. * This requires that your reducers update all nested properties in * an immutable way, which is required practice with Redux and also with reactive-state. * To make the observable emit any time the state changes, use .select() instead * For correct nested reducer updates, see: * http://redux.js.org/docs/recipes/reducers/ImmutableUpdatePatterns.html#updating-nested-objects * * @param selectorFn A selector function which returns a mapped/transformed object based on the state * @returns An observable that emits the result of the selector function after a * change of the return value of the selector function */ Store.prototype.watch = function (selectorFn) { return this.select(selectorFn).pipe(operators_1.distinctUntilChanged(function (a, b) { return shallowEqual_1.shallowEqual(a, b); })); }; /** * Same as .watch() except that EVERY state change is emitted. Use with care, you might want to pipe the output * to your own implementation of .distinctUntilChanged() or use only for debugging purposes. */ Store.prototype.select = function (selectorFn) { if (!selectorFn) selectorFn = function (state) { return state; }; var mapped = this.state.pipe(operators_1.takeUntil(this._destroyed), operators_1.map(selectorFn)); return mapped; }; /** * Destroys the Store/Slice. All Observables obtained via .select() will complete when called. */ Store.prototype.destroy = function () { this._destroyed.next(undefined); this._destroyed.complete(); }; /** * Manually dispatch an action by its actionName and actionPayload. * * This function exists for compatibility reasons, development and devtools. It is not adviced to use * this function extensively. * * Note: While the observable-based actions * dispatches only reducers registered for that slice, the string based action dispatch here will forward the * action to ALL stores, (sub-)slice and parent alike so make sure you separate your actions based on the strings. */ Store.prototype.dispatch = function (actionName, actionPayload) { this.actionDispatch.next({ actionName: actionName, actionPayload: actionPayload }); }; return Store; }()); exports.Store = Store; function mutateRootState(rootState, forwardProjections, backwardProjections, sliceReducer) { // transform the rootstate to a slice by applying all forward projections var forwardState = rootState; var intermediaryState = [rootState]; forwardProjections.map(function (fp) { forwardState = fp.call(undefined, forwardState); intermediaryState.push(forwardState); }); // perform the reduction var reducedState = sliceReducer(forwardState); // apply all backward projections to obtain the root state again var backwardState = reducedState; __spreadArrays(backwardProjections).map(function (bp, index) { var intermediaryIndex = intermediaryState.length - index - 2; backwardState = bp.call(undefined, backwardState, intermediaryState[intermediaryIndex]); }); return backwardState; } //# sourceMappingURL=store.js.map