Skip to content
This repository has been archived by the owner on Dec 11, 2019. It is now read-only.

Fix tab auto-discarding ability #13120

Merged
merged 1 commit into from
Feb 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions app/browser/activeTabHistory.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ const api = {
*/
clearTabbedWindow: function (windowId) {
activeTabsByWindow.delete(windowId)
},

/**
* Replace all intances of `oldTabId` with `newTabId` in
* active tab history for each window
*/
tabIdChanged: function (oldTabId, newTabId) {
for (const [windowId, windowActiveTabs] of activeTabsByWindow) {
if (windowActiveTabs && windowActiveTabs.length) {
activeTabsByWindow.set(windowId, windowActiveTabs.map(previousTabId => (previousTabId === oldTabId) ? newTabId : previousTabId))
}
}
}
}

Expand Down
16 changes: 15 additions & 1 deletion app/browser/reducers/tabsReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const tabActionConsts = require('../../common/constants/tabAction')
const flash = require('../../../js/flash')
const {frameOptsFromFrame} = require('../../../js/state/frameStateUtil')
const {isSourceAboutUrl, isTargetAboutUrl, isNavigatableAboutPage} = require('../../../js/lib/appUrlUtil')
const {shouldDebugTabEvents} = require('../../cmdLine')

const WEBRTC_DEFAULT = 'default'
const WEBRTC_DISABLE_NON_PROXY = 'disable_non_proxied_udp'
Expand Down Expand Up @@ -150,6 +151,19 @@ const tabsReducer = (state, action, immutableAction) => {
state = tabState.maybeCreateTab(state, action)
// tabs.debugTabs(state)
break
case appConstants.APP_TAB_REPLACED:
if (action.get('isPermanent')) {
if (shouldDebugTabEvents) {
console.log('APP_TAB_REPLACED before')
tabs.debugTabs(state)
}
state = tabState.replaceTabValue(state, action.get('oldTabId'), action.get('newTabValue'))
if (shouldDebugTabEvents) {
console.log('APP_TAB_REPLACED after')
tabs.debugTabs(state)
}
}
break
case appConstants.APP_TAB_CLOSE_REQUESTED:
{
let tabId = action.get('tabId')
Expand Down Expand Up @@ -335,7 +349,7 @@ const tabsReducer = (state, action, immutableAction) => {
break
}
case appConstants.APP_FRAME_CHANGED:
state = tabState.updateFrame(state, action)
state = tabState.updateFrame(state, action, shouldDebugTabEvents)
break
case windowConstants.WINDOW_SET_FRAME_ERROR:
{
Expand Down
76 changes: 61 additions & 15 deletions app/browser/tabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,9 @@ const api = {
})

process.on('add-new-contents', (e, source, newTab, disposition, size, userGesture) => {
if (shouldDebugTabEvents) {
console.log(`Contents [${newTab.getId()}] process:add-new-contents`, {userGesture, isBackground: newTab.isBackgroundPage(), disposition})
}
if (userGesture === false) {
e.preventDefault()
return
Expand All @@ -491,7 +494,10 @@ const api = {
}

const tabId = newTab.getId()

// update our webContents Map with the openerTabId
webContentsCache.updateWebContents(tabId, newTab, openerTabId)

let newTabValue = getTabValue(newTab.getId())

let windowId
Expand Down Expand Up @@ -559,6 +565,10 @@ const api = {
}
const tabId = tab.getId()

// This is the first and most consistent event for WebContents so cache the item in the Map.
// Not all contents will get the 'add-new-contents' event (e.g. replaced contents during tab discard).
webContentsCache.updateWebContents(tabId, tab, null)

// command-line flag --debug-tab-events
if (shouldDebugTabEvents) {
console.log(`Tab [${tabId}] created in window ${tab.tabValue().windowId}`)
Expand Down Expand Up @@ -642,6 +652,29 @@ const api = {
}
})

tab.on('tab-replaced-at', (e, windowId, tabIndex, newContents) => {
// if not a placeholder, new contents is permanent replacement, e.g. tab has been discarded
// if is a placeholder, new contents is temporary, and should not be used for tab ID
const isPlaceholder = newContents.isPlaceholder()
const newTabId = newContents.getId()

if (shouldDebugTabEvents) {
if (isPlaceholder) {
console.log(`Tab [${tabId}] got a new placeholder (${newTabId}), not updating state.`)
} else {
console.log(`Tab [${tabId}] permanently changed to tabId ${newTabId}. Updating state references...`)
}
}

// update state
appActions.tabReplaced(tabId, getTabValue(newTabId), getTabValue(tabId).get('windowId'), !isPlaceholder)
if (!isPlaceholder) {
// update in-memory caches
webContentsCache.tabIdChanged(tabId, newTabId)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could introduce a race condition because the webContentsCache and activeTabHistory will be temporarily out-of-sync with the app state. Can we make them reducers instead that update on the tabReplaced action?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

activeTabHistory and webContentsCache[].openerTabId (openerTabId is the only prop changed here) are only called in response to tab events coming from muon, not app state actions. Is that enough to satisfy your concern @bridiver? My thinking was to change them immediately to actually avoid a race condition, in case we're getting the tab 'detached' / 'destroyed' event quicker than the store can update those values - and we need the values updated then so that the next active TabId can be correctly chosen. Or is your concern that the tabReplaced action may rely on that data?

Either way, I should test that detaching a tab correctly sets the appropriate parent tab on detach, because there may be an issue with the 'temporary' contents id, in which case even if isPlaceholder === true we should call webContentsCache.tabIdChanged and activeTabHistory.tabIdChanged since that is window-specific and the info is needed on tab.on('will-destroy') which is fired very soon after tab-id-changed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^ detaching is fine, and we shouldn't change the activehistory / openerTabId tabId for placeholder replacements since tabs.moveTo directly calls tabs.getNextActiveTabId passing in the moved tabId (and not the temporary replacement tabId) - so detaching and setting the appropriate next tab active in the departing window, depending on preference, works nicely.

activeTabHistory.tabIdChanged(tabId, newTabId)
}
})

tab.on('tab-strip-empty', () => {
// It's only safe to close a window when the last web-contents tab has been
// re-attached. A detach which already happens by this point is not enough.
Expand Down Expand Up @@ -914,6 +947,9 @@ const api = {
},

closeTab: (tabId, forceClosePinned = false) => {
if (shouldDebugTabEvents) {
console.log(`[${tabId}] tabs.closeTab(forceClosePinned: ${forceClosePinned})`)
}
const tabValue = getTabValue(tabId)
if (!tabValue) {
return false
Expand Down Expand Up @@ -979,12 +1015,6 @@ const api = {
const preventAutoDiscard = createProperties.pinned || !isRegularContent
if (preventAutoDiscard) {
createProperties.autoDiscardable = false
} else {
// (temporarily) forced autoDiscardable to ALWAYS false due to
// inability to switch to auto-discarded tabs.
// See https://github.com/brave/browser-laptop/issues/10673
// remove this forced 'else' condition when #10673 is resolved
createProperties.autoDiscardable = false
}

const doCreate = () => {
Expand Down Expand Up @@ -1042,6 +1072,9 @@ const api = {
},

moveTo: (state, tabId, frameOpts, browserOpts, toWindowId) => {
if (shouldDebugTabEvents) {
console.log(`Tab ${tabId}] tabs.moveTo(window: ${toWindowId})`)
}
frameOpts = makeImmutable(frameOpts)
browserOpts = makeImmutable(browserOpts)
const tab = webContentsCache.getWebContents(tabId)
Expand Down Expand Up @@ -1071,8 +1104,24 @@ const api = {
return
}

// detach from current window
// perform detach from current window
// and handle at `did-detach`
tab.detach(() => {
if (shouldDebugTabEvents) {
console.log(`Tab [${tabId}] detached tab guestinstance id`, tab.guestInstanceId)
}
frameOpts = frameOpts.set('guestInstanceId', tab.guestInstanceId)
// handle tab has made it to the new window
tab.once('did-attach', () => {
if (shouldDebugTabEvents) {
console.log(`Tab [${tabId}] attached to a new window, so setting the desired index`)
}
// put the tab in the desired index position
const index = frameOpts.get('index')
if (index !== undefined) {
api.setTabIndex(tabId, frameOpts.get('index'))
}
})
// handle tab has detached from window
// handle tab was the active tab of the window
if (tabValue.get('active')) {
Expand All @@ -1082,7 +1131,12 @@ const api = {
api.setActive(nextActiveTabIdForOldWindow)
}
}
// tell another window to add the tab, this will cause the tab to render a webview and
// then attach to that window
if (toWindowId == null || toWindowId === -1) {
if (shouldDebugTabEvents) {
console.log('creating new window for detached tab')
}
// move tab to a new window
frameOpts = frameOpts.set('index', 0)
appActions.newWindow(frameOpts, browserOpts)
Expand All @@ -1091,14 +1145,6 @@ const api = {
// specified window
appActions.newWebContentsAdded(toWindowId, frameOpts, tabValue)
}
// handle tab has made it to the new window
tab.once('did-attach', () => {
// put the tab in the desired index position
const index = frameOpts.get('index')
if (index !== undefined) {
api.setTabIndex(tabId, frameOpts.get('index'))
}
})
})
}
},
Expand Down
18 changes: 18 additions & 0 deletions app/browser/webContentsCache.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,29 @@ const forgetOpenerForTabId = (tabId) => {
}
}

const tabIdChanged = (oldTabId, newTabId) => {
// any tabs referencing the old contents Id as the opener,
// should now reference the new contents Id
for (const tabId in currentWebContents) {
const tabData = currentWebContents[tabId]
if (tabData && tabData.openerTabId != null && tabData.openerTabId === oldTabId) {
tabData.openerTabId = newTabId
}
}
// we should also give the replacement tab the opener for the old tab
const newTabData = currentWebContents[newTabId]
const oldTabData = currentWebContents[oldTabId]
if (newTabData && oldTabData && oldTabData.openerTabId != null) {
newTabData.openerTabId = oldTabData.openerTabId
}
}

module.exports = {
cleanupWebContents,
getWebContents,
getOpenerTabId,
forgetOpenerForTabId,
updateWebContents,
tabIdChanged,
currentWebContents
}
38 changes: 37 additions & 1 deletion app/common/state/tabState.js
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,31 @@ const tabState = {
return state.set('tabs', tabs.delete(index).insert(index, tabValue))
},

replaceTabValue: (state, tabId, newTabValue) => {
state = validateState(state)
newTabValue = makeImmutable(newTabValue)
// update tab
const index = getTabInternalIndexByTabId(state, tabId)
const oldTabValue = state.getIn(['tabs', index])
if (index == null || index === -1) {
console.error(`tabState: cannot replace tab ${tabId} as tab's index did not exist in state`, { index })
return state
}
let mergedTabValue = oldTabValue.mergeDeep(newTabValue)
if (mergedTabValue.has('frame')) {
mergedTabValue = mergedTabValue.mergeIn(['frame'], {
tabId: newTabValue.get('tabId'),
guestInstanceId: newTabValue.get('guestInstanceId')
})
}
mergedTabValue = mergedTabValue.set('windowId', oldTabValue.get('windowId'))
state = state.set('tabs', state.get('tabs').delete(index).insert(index, mergedTabValue))
// update tabId at tabsInternal index
state = deleteTabsInternalIndex(state, oldTabValue)
state = updateTabsInternalIndex(state, 0)
return state
},

removeTabField: (state, field) => {
state = makeImmutable(state)

Expand All @@ -417,19 +442,30 @@ const tabState = {
return state.set('tabs', tabs)
},

updateFrame: (state, action) => {
updateFrame: (state, action, shouldDebugTabEvents = false) => {
state = validateState(state)
action = validateAction(action)
const tabId = action.getIn(['frame', 'tabId'])

if (!tabId) {
if (shouldDebugTabEvents) {
console.log(`Tab [${tabId}] frame changed for tab - no tabId provided!`)
}
return state
}

let tabValue = tabState.getByTabId(state, tabId)
if (!tabValue) {
if (shouldDebugTabEvents) {
console.log(`Tab [${tabId}] frame changed for tab - tab not found in state, probably a temporary frame`)
}
return state
}

if (shouldDebugTabEvents) {
console.log(`Tab [${tabId}] frame changed for tab`)
}

const bookmarkUtil = require('../lib/bookmarkUtil')
const frameLocation = action.getIn(['frame', 'location'])
const frameBookmarked = bookmarkUtil.isLocationBookmarked(state, frameLocation)
Expand Down
13 changes: 6 additions & 7 deletions app/renderer/components/frame/frame.js
Original file line number Diff line number Diff line change
Expand Up @@ -433,13 +433,9 @@ class Frame extends React.Component {
}

addEventListeners () {
this.webview.addEventListener('tab-id-changed', (e) => {
if (this.frame.isEmpty()) {
return
}

windowActions.frameTabIdChanged(this.frame, this.props.tabId, e.tabID)
}, { passive: true })
// Webview also exposes the 'tab-id-changed' event, with e.tabID as the new tabId.
// We don't handle that event anymore, in favor of tab-replaced-at in the browser process.
// Keeping this comment here as it is not documented - petemill.
this.webview.addEventListener('guest-ready', (e) => {
if (this.frame.isEmpty()) {
return
Expand Down Expand Up @@ -938,6 +934,9 @@ class Frame extends React.Component {
const transitionClassName = this.getTransitionStateClassName(this.props.transitionState)
return <div
data-partition={this.props.partition}
data-tab-id={this.props.tabId}
data-frame-key={this.props.frameKey}
data-guest-id={this.props.guestInstanceId}
data-test-id='frameWrapper'
data-test2-id={this.props.isActive ? 'activeFrame' : null}
data-test3-id={this.props.isPreview ? 'previewFrame' : null}
Expand Down
12 changes: 12 additions & 0 deletions js/actions/appActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,18 @@ const appActions = {
})
},

tabReplaced: function (oldTabId, newTabValue, windowId, isPermanent) {
dispatch({
actionType: appConstants.APP_TAB_REPLACED,
oldTabId,
newTabValue,
isPermanent,
queryInfo: {
windowId
}
})
},

/**
* Dispatches a message to the store to set a new frame as the active frame.
*
Expand Down
1 change: 1 addition & 0 deletions js/constants/appConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const appConstants = {
APP_TAB_STRIP_EMPTY: _,
APP_TAB_CLOSED: _,
APP_TAB_UPDATED: _,
APP_TAB_REPLACED: _,
APP_ADD_HISTORY_SITE: _,
APP_SET_STATE: _,
APP_REMOVE_HISTORY_SITE: _,
Expand Down
13 changes: 10 additions & 3 deletions js/stores/windowStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,14 +175,21 @@ const newFrame = (state, frameOpts) => {
const frameTabIdChanged = (state, action) => {
action = makeImmutable(action)
const oldTabId = action.get('oldTabId')
const newTabId = action.get('newTabId')
const newTabValue = action.get('newTabValue')
const newTabId = newTabValue.get('tabId')
if (newTabId == null || oldTabId === newTabId) {
console.error('Invalid action arguments for frameTabIdChanged')
return state
}

let newFrameProps = new Immutable.Map()
newFrameProps = newFrameProps.set('tabId', newTabId)
const index = frameStateUtil.getFrameIndex(state, action.getIn(['frameProps', 'key']))
const frame = frameStateUtil.getFrameByTabId(state, oldTabId)
if (!frame) {
console.error(`Could not find frame with tabId ${oldTabId} in order to replace with new tabId ${newTabId}`)
return state
}
const index = frameStateUtil.getFrameIndex(state, frame.get('key'))
state = state.mergeIn(['frames', index], newFrameProps)
state = frameStateUtil.deleteTabInternalIndex(state, oldTabId)
state = frameStateUtil.updateFramesInternalIndex(state, index)
Expand Down Expand Up @@ -261,7 +268,7 @@ const doAction = (action) => {
}
// We should not emit here because the Window already know about the change on startup.
return
case windowConstants.WINDOW_FRAME_TAB_ID_CHANGED:
case appConstants.APP_TAB_REPLACED:
windowState = frameTabIdChanged(windowState, action)
break
case windowConstants.WINDOW_FRAME_GUEST_INSTANCE_ID_CHANGED:
Expand Down