Skip to content

Commit

Permalink
fix(reducers): removed debouncing from reduceState
Browse files Browse the repository at this point in the history
  BREAKING CHANGE: removed debouncing in reduceState and started
  emitting all state calculations.

  This has some implications in the sense that defaultState will
  always be emitted and reduceState might sometimes have some
  "redundant" emissions. See the tests (and the failing test)
  in the commit for details.

  Debouncing state streams made it unsafe to use withLatestFrom,
  since the stream calculations were debounced.
  • Loading branch information
anton164 committed Mar 26, 2020
1 parent c989326 commit 60f9b99
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 18 deletions.
66 changes: 55 additions & 11 deletions src/operators/reduceState.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ test(
);

test(
'reduceState should only emit the latest state processed at the current frame',
'reduceState always emits defaultState and every subsequent calculation',
marbles(m => {
const action$ = m.hot(' --1-', actions);
const word$ = m.hot(' a-a ', words);
const expected$ = m.hot('2-4', numbers);
const action$ = m.hot(' -----1-', actions);
const word$ = m.hot(' a----a ', words);
const expected$ = m.hot('(12)-(34)', numbers);
const defaultState = 1;

const handleWord = reducer(
Expand Down Expand Up @@ -103,9 +103,9 @@ test(
test(
'reduceState catches errors and emits them to error subject',
marbles(m => {
const action$ = m.hot(' 1d1', actions);
const expected$ = m.hot('2-3', numbers);
const errorMarbles = ' -e-';
const action$ = m.hot(' -1d--1', actions);
const expected$ = m.hot('12---3', numbers);
const errorMarbles = ' --e-';
const error$ = new Subject<any>();

m.expect(error$).toBeObservable(errorMarbles, errors);
Expand All @@ -119,9 +119,9 @@ test(
'reduceState replays values to new subscribers',
marbles(m => {
const defaultState = 1;
const action$ = m.hot(' 1--1----------', actions);
const action$ = m.hot(' -1-1----------', actions);
const sub1 = ' ^-------------';
const sub1Expected$ = m.hot('2--3--------', numbers);
const sub1Expected$ = m.hot('12-3--------', numbers);
const sub2 = ' ------^-------';
const sub2Expected$ = m.hot('------3-------', numbers);
const state$ = action$.pipe(
Expand Down Expand Up @@ -158,14 +158,14 @@ test(
);

test(
'reduceState emits outdated value to derived streams due to debouncing',
'reduceState emits updated value to derived streams',
marbles(m => {
const defaultState = 1;
const action$ = m.hot(' -2-(12)-2-', actions);
const sub1 = ' ^---------';
const sub1Expected$ = m.hot('1--2------', numbers);
const sub2 = ' -^--------';
const sub2Expected$ = m.hot('-1-1----2-', numbers);
const sub2Expected$ = m.hot('-1-2----2-', numbers);

const state$ = action$.pipe(
reduceState('testStream', defaultState, [reducers.handleOne])
Expand All @@ -180,3 +180,47 @@ test(
m.expect(routine$, sub2).toBeObservable(sub2Expected$);
})
);

test(
'reduceState emits updated value to derived streams if routine subscribes first',
marbles(m => {
const defaultState = 1;
const action$ = m.hot(' 2--(12)-2-', actions);
const sub1 = ' -^--------';
const sub1Expected$ = m.hot('-1-2------', numbers);
const sub2 = ' ^---------';
const sub2Expected$ = m.hot('1--2----2-', numbers);

const state$ = action$.pipe(
reduceState('testStream', defaultState, [reducers.handleOne])
);
const routine$ = action$.pipe(
ofType(incrementMocks.actionCreators.incrementMany),
withLatestFrom(state$),
map(([, s]) => s)
);

m.expect(state$, sub1).toBeObservable(sub1Expected$);
m.expect(routine$, sub2).toBeObservable(sub2Expected$);
})
);

test.failing(
'reduceState should only emit the latest state processed at the current frame to prevent glitches',
marbles(m => {
const action$ = m.hot(' --1-', actions);
const word$ = m.hot(' a-a ', words);
const expected$ = m.hot('2-4', numbers);
const defaultState = 1;

const handleWord = reducer(
word$,
(state: number, word) => state + word.length
);
const state$ = action$.pipe(
reduceState('testStream', defaultState, [...reducerArray, handleWord])
);

m.expect(state$).toBeObservable(expected$);
})
);
9 changes: 2 additions & 7 deletions src/operators/reduceState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { markName } from '../internal/markers';
import { UnknownAction } from '../internal/types';
import { defaultErrorSubject } from '../internal/defaultErrorSubject';
import { tag } from 'rxjs-spy/operators';
import { shareReplay, startWith, debounceTime } from 'rxjs/operators';
import { shareReplay, startWith } from 'rxjs/operators';
import { combineReducers, RegisteredReducer } from '../reducer';
import { OperatorFunction, pipe, Subject } from 'rxjs';

Expand All @@ -19,11 +19,7 @@ import { OperatorFunction, pipe, Subject } from 'rxjs';
*
* The values emitted from the stream are shared between the subscribers,
* and the reducers are only ran once per input action.
*
* All emissions from the state stream are debounced to ensure that the stream
* doesn't emit redundant state when multiple reducers are triggered in the same
* frame. Note that this might have impliciations for listeners that are reading
* the latest value of the stream directly after the reducers have been triggered.
*.
*
* @param name A name for debugging purposes
* @param action$ The action stream
Expand All @@ -43,7 +39,6 @@ export const reduceState = <State>(
pipe(
combineReducers(defaultState, reducers, errorSubject),
startWith(defaultState),
debounceTime(0),
shareReplay({
refCount: true,
bufferSize: 1,
Expand Down

0 comments on commit 60f9b99

Please sign in to comment.