@@ -77,15 +77,32 @@ function computeNextEntry(reducer, action, state, error) {
7777}
7878
7979/**
80- * Runs the reducer on all actions to get a fresh computation log.
80+ * Runs the reducer on invalidated actions to get a fresh computation log.
8181 */
82- function recomputeStates ( reducer , committedState , actionsById , stagedActionIds , skippedActionIds ) {
83- const computedStates = [ ] ;
84- for ( let i = 0 ; i < stagedActionIds . length ; i ++ ) {
82+ function recomputeStates (
83+ computedStates ,
84+ minInvalidatedStateIndex ,
85+ reducer ,
86+ committedState ,
87+ actionsById ,
88+ stagedActionIds ,
89+ skippedActionIds
90+ ) {
91+ // Optimization: exit early and return the same reference
92+ // if we know nothing could have changed.
93+ if (
94+ minInvalidatedStateIndex >= computedStates . length &&
95+ computedStates . length === stagedActionIds . length
96+ ) {
97+ return computedStates ;
98+ }
99+
100+ const nextComputedStates = computedStates . slice ( 0 , minInvalidatedStateIndex ) ;
101+ for ( let i = minInvalidatedStateIndex ; i < stagedActionIds . length ; i ++ ) {
85102 const actionId = stagedActionIds [ i ] ;
86103 const action = actionsById [ actionId ] . action ;
87104
88- const previousEntry = computedStates [ i - 1 ] ;
105+ const previousEntry = nextComputedStates [ i - 1 ] ;
89106 const previousState = previousEntry ? previousEntry . state : committedState ;
90107 const previousError = previousEntry ? previousEntry . error : undefined ;
91108
@@ -94,10 +111,10 @@ function recomputeStates(reducer, committedState, actionsById, stagedActionIds,
94111 previousEntry :
95112 computeNextEntry ( reducer , action , previousState , previousError ) ;
96113
97- computedStates . push ( entry ) ;
114+ nextComputedStates . push ( entry ) ;
98115 }
99116
100- return computedStates ;
117+ return nextComputedStates ;
101118}
102119
103120/**
@@ -121,14 +138,13 @@ function liftReducerWith(reducer, initialCommittedState, monitorReducer) {
121138 skippedActionIds : [ ] ,
122139 committedState : initialCommittedState ,
123140 currentStateIndex : 0 ,
124- computedStates : undefined
141+ computedStates : [ ]
125142 } ;
126143
127144 /**
128145 * Manages how the history actions modify the history state.
129146 */
130147 return ( liftedState = initialLiftedState , liftedAction ) => {
131- let shouldRecomputeStates = true ;
132148 let {
133149 monitorState,
134150 actionsById,
@@ -140,116 +156,125 @@ function liftReducerWith(reducer, initialCommittedState, monitorReducer) {
140156 computedStates
141157 } = liftedState ;
142158
159+ // By default, agressively recompute every state whatever happens.
160+ // This has O(n) performance, so we'll override this to a sensible
161+ // value whenever we feel like we don't have to recompute the states.
162+ let minInvalidatedStateIndex = 0 ;
163+
143164 switch ( liftedAction . type ) {
144- case ActionTypes . RESET :
145- actionsById = {
146- 0 : liftAction ( INIT_ACTION )
147- } ;
148- nextActionId = 1 ;
149- stagedActionIds = [ 0 ] ;
150- skippedActionIds = [ ] ;
151- committedState = initialCommittedState ;
152- currentStateIndex = 0 ;
153- break ;
154- case ActionTypes . COMMIT :
155- actionsById = {
156- 0 : liftAction ( INIT_ACTION )
157- } ;
158- nextActionId = 1 ;
159- stagedActionIds = [ 0 ] ;
160- skippedActionIds = [ ] ;
161- committedState = computedStates [ currentStateIndex ] . state ;
162- currentStateIndex = 0 ;
163- break ;
164- case ActionTypes . ROLLBACK :
165- actionsById = {
166- 0 : liftAction ( INIT_ACTION )
167- } ;
168- nextActionId = 1 ;
169- stagedActionIds = [ 0 ] ;
170- skippedActionIds = [ ] ;
171- currentStateIndex = 0 ;
172- break ;
173- case ActionTypes . TOGGLE_ACTION :
174- const index = skippedActionIds . indexOf ( liftedAction . id ) ;
175- if ( index === - 1 ) {
176- skippedActionIds = [
177- liftedAction . id ,
178- ...skippedActionIds
179- ] ;
180- } else {
181- skippedActionIds = [
182- ...skippedActionIds . slice ( 0 , index ) ,
183- ...skippedActionIds . slice ( index + 1 )
184- ] ;
165+ case ActionTypes . RESET : {
166+ // Get back to the state the store was created with.
167+ actionsById = { 0 : liftAction ( INIT_ACTION ) } ;
168+ nextActionId = 1 ;
169+ stagedActionIds = [ 0 ] ;
170+ skippedActionIds = [ ] ;
171+ committedState = initialCommittedState ;
172+ currentStateIndex = 0 ;
173+ computedStates = [ ] ;
174+ break ;
185175 }
186- break ;
187- case ActionTypes . JUMP_TO_STATE :
188- currentStateIndex = liftedAction . index ;
189- // Optimization: we know the history has not changed.
190- shouldRecomputeStates = false ;
191- break ;
192- case ActionTypes . SWEEP :
193- stagedActionIds = difference ( stagedActionIds , skippedActionIds ) ;
194- skippedActionIds = [ ] ;
195- currentStateIndex = Math . min ( currentStateIndex , stagedActionIds . length - 1 ) ;
196- break ;
197- case ActionTypes . PERFORM_ACTION :
198- if ( currentStateIndex === stagedActionIds . length - 1 ) {
199- currentStateIndex ++ ;
176+ case ActionTypes . COMMIT : {
177+ // Consider the last committed state the new starting point.
178+ // Squash any staged actions into a single committed state.
179+ actionsById = { 0 : liftAction ( INIT_ACTION ) } ;
180+ nextActionId = 1 ;
181+ stagedActionIds = [ 0 ] ;
182+ skippedActionIds = [ ] ;
183+ committedState = computedStates [ currentStateIndex ] . state ;
184+ currentStateIndex = 0 ;
185+ computedStates = [ ] ;
186+ break ;
187+ }
188+ case ActionTypes . ROLLBACK : {
189+ // Forget about any staged actions.
190+ // Start again from the last committed state.
191+ actionsById = { 0 : liftAction ( INIT_ACTION ) } ;
192+ nextActionId = 1 ;
193+ stagedActionIds = [ 0 ] ;
194+ skippedActionIds = [ ] ;
195+ currentStateIndex = 0 ;
196+ computedStates = [ ] ;
197+ break ;
198+ }
199+ case ActionTypes . TOGGLE_ACTION : {
200+ // Toggle whether an action with given ID is skipped.
201+ // Being skipped means it is a no-op during the computation.
202+ const { id : actionId } = liftedAction ;
203+ const index = skippedActionIds . indexOf ( actionId ) ;
204+ if ( index === - 1 ) {
205+ skippedActionIds = [ actionId , ...skippedActionIds ] ;
206+ } else {
207+ skippedActionIds = skippedActionIds . filter ( id => id !== actionId ) ;
208+ }
209+ // Optimization: we know history before this action hasn't changed
210+ minInvalidatedStateIndex = stagedActionIds . indexOf ( actionId ) ;
211+ break ;
212+ }
213+ case ActionTypes . JUMP_TO_STATE : {
214+ // Without recomputing anything, move the pointer that tell us
215+ // which state is considered the current one. Useful for sliders.
216+ currentStateIndex = liftedAction . index ;
217+ // Optimization: we know the history has not changed.
218+ minInvalidatedStateIndex = Infinity ;
219+ break ;
220+ }
221+ case ActionTypes . SWEEP : {
222+ // Forget any actions that are currently being skipped.
223+ stagedActionIds = difference ( stagedActionIds , skippedActionIds ) ;
224+ skippedActionIds = [ ] ;
225+ currentStateIndex = Math . min ( currentStateIndex , stagedActionIds . length - 1 ) ;
226+ break ;
227+ }
228+ case ActionTypes . PERFORM_ACTION : {
229+ if ( currentStateIndex === stagedActionIds . length - 1 ) {
230+ currentStateIndex ++ ;
231+ }
232+ const actionId = nextActionId ++ ;
233+ // Mutation! This is the hottest path, and we optimize on purpose.
234+ // It is safe because we set a new key in a cache dictionary.
235+ actionsById [ actionId ] = liftedAction ;
236+ stagedActionIds = [ ...stagedActionIds , actionId ] ;
237+ // Optimization: we know that only the new action needs computing.
238+ minInvalidatedStateIndex = stagedActionIds . length - 1 ;
239+ break ;
240+ }
241+ case ActionTypes . IMPORT_STATE : {
242+ // Completely replace everything.
243+ ( {
244+ monitorState,
245+ actionsById,
246+ nextActionId,
247+ stagedActionIds,
248+ skippedActionIds,
249+ committedState,
250+ currentStateIndex,
251+ computedStates
252+ } = liftedAction . nextLiftedState ) ;
253+ break ;
254+ }
255+ case '@@redux/INIT' : {
256+ // Always recompute states on hot reload and init.
257+ minInvalidatedStateIndex = 0 ;
258+ break ;
259+ }
260+ default : {
261+ // If the action is not recognized, it's a monitor action.
262+ // Optimization: a monitor action can't change history.
263+ minInvalidatedStateIndex = Infinity ;
264+ break ;
200265 }
201-
202- const actionId = nextActionId ++ ;
203- // Mutation! This is the hottest path, and we optimize on purpose.
204- // It is safe because we set a new key in a cache dictionary.
205- actionsById [ actionId ] = liftedAction ;
206- stagedActionIds = [ ...stagedActionIds , actionId ] ;
207- // Optimization: we know that the past has not changed.
208- shouldRecomputeStates = false ;
209- // Instead of recomputing the states, append the next one.
210- const previousEntry = computedStates [ computedStates . length - 1 ] ;
211- const nextEntry = computeNextEntry (
212- reducer ,
213- liftedAction . action ,
214- previousEntry . state ,
215- previousEntry . error
216- ) ;
217- computedStates = [ ...computedStates , nextEntry ] ;
218- break ;
219- case ActionTypes . IMPORT_STATE :
220- ( {
221- monitorState,
222- actionsById,
223- nextActionId,
224- stagedActionIds,
225- skippedActionIds,
226- committedState,
227- currentStateIndex,
228- computedStates
229- } = liftedAction . nextLiftedState ) ;
230- break ;
231- case '@@redux/INIT' :
232- // Always recompute states on hot reload and init.
233- shouldRecomputeStates = true ;
234- break ;
235- default :
236- // Optimization: a monitor action can't change history.
237- shouldRecomputeStates = false ;
238- break ;
239- }
240-
241- if ( shouldRecomputeStates ) {
242- computedStates = recomputeStates (
243- reducer ,
244- committedState ,
245- actionsById ,
246- stagedActionIds ,
247- skippedActionIds
248- ) ;
249266 }
250267
268+ computedStates = recomputeStates (
269+ computedStates ,
270+ minInvalidatedStateIndex ,
271+ reducer ,
272+ committedState ,
273+ actionsById ,
274+ stagedActionIds ,
275+ skippedActionIds
276+ ) ;
251277 monitorState = monitorReducer ( monitorState , liftedAction ) ;
252-
253278 return {
254279 monitorState,
255280 actionsById,
0 commit comments