EngineStateReducer.kt
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.browser.state.reducer
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.EngineState
import mozilla.components.browser.state.state.SessionState
internal object EngineStateReducer {
// Maximum number of recently killed tabs to retain in memory.
// When this limit is exceeded, the oldest entry is removed to maintain a fixed size.
private const val MAX_RECENTLY_KILLED_TABS = 50
/**
* [EngineAction] Reducer function for modifying a specific [EngineState]
* of a [SessionState].
*/
@Suppress("LongMethod")
fun reduce(state: BrowserState, action: EngineAction): BrowserState {
return when (action) {
is EngineAction.LinkEngineSessionAction -> state.copyWithEngineState(action.tabId) {
it.copy(
engineSession = action.engineSession,
timestamp = action.timestamp,
)
}
is EngineAction.UnlinkEngineSessionAction -> state.copyWithEngineState(action.tabId) {
it.copy(
engineSession = null,
engineObserver = null,
)
}
is EngineAction.UpdateEngineSessionObserverAction -> state.copyWithEngineState(action.tabId) {
it.copy(engineObserver = action.engineSessionObserver)
}
is EngineAction.UpdateEngineSessionStateAction -> state.copyWithEngineState(action.tabId) { engineState ->
if (engineState.crashed) {
// We ignore state updates for a crashed engine session. We want to keep the last state until
// this tab gets restored (or closed).
engineState
} else {
engineState.copy(engineSessionState = action.engineSessionState)
}
}
is EngineAction.UpdateEngineSessionInitializingAction -> state.copyWithEngineState(action.tabId) {
it.copy(initializing = action.initializing)
}
is EngineAction.OptimizedLoadUrlTriggeredAction -> {
state
}
is EngineAction.SaveToPdfExceptionAction,
is EngineAction.SaveToPdfCompleteAction,
-> {
throw IllegalStateException(
"You need to add a middleware to handle this action in your BrowserStore. ($action)",
)
}
is EngineAction.SuspendEngineSessionAction,
is EngineAction.LoadDataAction,
is EngineAction.LoadUrlAction,
is EngineAction.ReloadAction,
is EngineAction.GoBackAction,
is EngineAction.GoForwardAction,
is EngineAction.GoToHistoryIndexAction,
is EngineAction.ToggleDesktopModeAction,
is EngineAction.ExitFullScreenModeAction,
is EngineAction.SaveToPdfAction,
is EngineAction.PrintContentAction,
is EngineAction.PrintContentCompletedAction,
is EngineAction.PrintContentExceptionAction,
is EngineAction.ClearDataAction,
-> {
throw IllegalStateException("You need to add EngineMiddleware to your BrowserStore. ($action)")
}
is EngineAction.PurgeHistoryAction -> {
state.copy(
tabs = purgeEngineStates(state.tabs),
customTabs = purgeEngineStates(state.customTabs),
)
}
is EngineAction.KillEngineSessionAction -> {
val updatedKilledTabs = LinkedHashSet(state.recentlyKilledTabs)
updatedKilledTabs.add(action.tabId)
// Enforce max size of 50 recently killed tabs
if (updatedKilledTabs.size > MAX_RECENTLY_KILLED_TABS) {
val oldestEntry = updatedKilledTabs.first()
updatedKilledTabs.remove(oldestEntry)
}
state.copy(recentlyKilledTabs = updatedKilledTabs)
}
is EngineAction.CreateEngineSessionAction -> {
if (state.recentlyKilledTabs.isEmpty()) return state
val updatedKilledTabs = LinkedHashSet(state.recentlyKilledTabs)
updatedKilledTabs.remove(action.tabId)
state.copy(recentlyKilledTabs = updatedKilledTabs)
}
}
}
}
private inline fun BrowserState.copyWithEngineState(
tabId: String,
crossinline update: (EngineState) -> EngineState,
): BrowserState {
return updateTabOrCustomTabState(tabId) { current ->
current.createCopy(engineState = update(current.engineState))
}
}
/**
* When `PurgeHistoryAction` gets dispatched `EngineDelegateMiddleware` will take care of calling
* `purgeHistory()` on all `EngineSession` instances. However some tabs may not have an `EngineSession`
* assigned (yet), instead we keep track of the `EngineSessionState` to restore when needed. Creating
* an `EngineSession` for every tab, just to call `purgeHistory()` on them, is wasteful and may cause
* problems if there are a lot of tabs. So instead we just remove the EngineSessionState from those
* sessions. The next time they get rendered we will only load the assigned URL and since they have
* no state to restore, they will have no history.
*/
@Suppress("UNCHECKED_CAST")
private fun <T : SessionState> purgeEngineStates(tabs: List<T>): List<T> {
return tabs.map { session ->
if (session.engineState.engineSession == null && session.engineState.engineSessionState != null) {
session.createCopy(engineState = session.engineState.copy(engineSessionState = null))
} else {
session
} as T
}
}