Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix crash when there are undefined references in the state tree e.g. when a track is deleted but still referred to by a session #1597

Merged
merged 8 commits into from
Jan 8, 2021
33 changes: 20 additions & 13 deletions plugins/dotplot-view/src/DotplotDisplay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
makeAbortableReaction,
} from '@jbrowse/core/util'

import { autorun } from 'mobx'
import ServerSideRenderedBlockContent from '../ServerSideRenderedBlockContent'
import { DotplotViewModel } from '../DotplotView/model'

Expand Down Expand Up @@ -71,19 +72,25 @@ export function stateModelFactory(configSchema: any) {

return {
afterAttach() {
makeAbortableReaction(
self as any,
renderBlockData,
renderBlockEffect as any,
{
name: `${self.type} ${self.id} rendering`,
delay: 1000,
fireImmediately: true,
},
this.setLoading,
this.setRendered,
this.setError,
)
const parent = getContainingView(self) as DotplotViewModel
autorun(reaction => {
if (parent.initialized) {
makeAbortableReaction(
self as any,
renderBlockData,
renderBlockEffect as any,
{
name: `${self.type} ${self.id} rendering`,
delay: 1000,
fireImmediately: true,
},
this.setLoading,
this.setRendered,
this.setError,
)
reaction.dispose()
}
})
Copy link
Contributor

Choose a reason for hiding this comment

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

This nested autorun-reaction thing is a little frightening, I'm not sure what it might do in corner cases.

What if you added the if(parent.initialized) logic to the renderBlockData and renderBlockEffect functions instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll try that out

},

setLoading(abortController: AbortController) {
Expand Down
13 changes: 12 additions & 1 deletion products/jbrowse-web/src/Loader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,18 @@ const Renderer = observer(
)
}
} else if (sessionSnapshot) {
rootModel.setSession(loader.sessionSnapshot)
try {
rootModel.setSession(loader.sessionSnapshot)
} catch (err) {
console.error(err)
rootModel.setDefaultSession()
const errorMessage = (err.message || '')
.replace('[mobx-state-tree] ', '')
.replace(/\(.+/, '')
rootModel.session?.notify(
`Session could not be loaded. ${errorMessage}`,
)
}
} else {
const defaultJBrowseSession = rootModel.jbrowse.defaultSession
if (defaultJBrowseSession?.views) {
Expand Down
74 changes: 74 additions & 0 deletions products/jbrowse-web/src/rootModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ import {
getParent,
SnapshotIn,
types,
IAnyStateTreeNode,
Instance,
getType,
isArrayType,
isModelType,
isReferenceType,
isValidReference,
getPropertyMembers,
isMapType,
getChildType,
IAnyType,
} from 'mobx-state-tree'
import { observable, autorun } from 'mobx'
import { UndoManager } from 'mst-middlewares'
Expand All @@ -25,6 +36,58 @@ import jbrowseWebFactory from './jbrowseModel'
import RenderWorker from './rpc.worker'
import sessionModelFactory from './sessionModelFactory'

// attempts to remove undefined references from the given MST model. can only actually
// remove them from arrays and maps. throws MST undefined ref error if it encounters
// undefined refs in model properties
function filterSessionInPlace(node: IAnyStateTreeNode, nodeType: IAnyType) {
type MSTArray = Instance<ReturnType<typeof types.array>>
type MSTMap = Instance<ReturnType<typeof types.map>>

// makes it work with session sharing
if (node === undefined) {
return
}
if (isArrayType(nodeType)) {
const array = node as MSTArray
const childType = getChildType(node)
if (isReferenceType(childType)) {
// filter array elements
for (let i = 0; i < array.length; ) {
if (!isValidReference(() => array[i])) {
array.splice(i, 1)
} else {
i += 1
}
}
}
array.forEach(el => {
filterSessionInPlace(el, childType)
})
} else if (isMapType(nodeType)) {
const map = node as MSTMap
const childType = getChildType(map)
if (isReferenceType(childType)) {
// filter the map members
for (const key in map.keys()) {
if (!isValidReference(() => map.get(key))) {
map.delete(key)
}
}
}
map.forEach(child => {
filterSessionInPlace(child, childType)
})
} else if (isModelType(nodeType)) {
// iterate over children
const { properties } = getPropertyMembers(node)

Object.entries(properties).forEach(([pname, ptype]) => {
// @ts-ignore
filterSessionInPlace(node[pname], ptype)
})
}
}

interface Menu {
label: string
menuItems: MenuItem[]
Expand Down Expand Up @@ -151,7 +214,18 @@ export default function RootModel(
)
},
setSession(sessionSnapshot?: SnapshotIn<typeof Session>) {
const oldSession = self.session
self.session = cast(sessionSnapshot)
if (self.session) {
// validate all references in the session snapshot
try {
filterSessionInPlace(self.session, getType(self.session))
} catch (error) {
// throws error if session filtering failed
self.session = oldSession
throw error
}
}
},
setAssemblyEditing(flag: boolean) {
self.isAssemblyEditing = flag
Expand Down
2 changes: 1 addition & 1 deletion products/jbrowse-web/src/tests/Dotplot.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('dotplot view', () => {
)
const { findByTestId } = render(<JBrowse pluginManager={pluginManager} />)

const canvas = await findByTestId('prerendered_canvas', { timeout: 10000 })
const canvas = await findByTestId('prerendered_canvas', { timeout: 20000 })

const img = canvas.toDataURL()
const data = img.replace(/^data:image\/\w+;base64,/, '')
Expand Down