reactive-state
Version:
Redux-like state management using RxJS and TypeScript
261 lines • 14.3 kB
JavaScript
;
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