Skip to content

Commit ddee75f

Browse files
Move checks to track menu, make work for local
1 parent b37313c commit ddee75f

8 files changed

Lines changed: 185 additions & 101 deletions

File tree

packages/jbrowse-plugin-apollo/src/BackendDrivers/BackendDriver.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,6 @@ export abstract class BackendDriver {
4949
): Promise<AnnotationFeatureSnapshot[]>
5050

5151
abstract getChanges(assemblyName: string): Promise<ChangeDocument[]>
52+
53+
abstract getCheckResults(assemblyName: string): Promise<CheckResultSnapshot[]>
5254
}

packages/jbrowse-plugin-apollo/src/BackendDrivers/CollaborationServerDriver.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,22 @@ export class CollaborationServerDriver extends BackendDriver {
412412
return response.json() as Promise<ChangeDocument[]>
413413
}
414414

415+
async getCheckResults(assemblyName: string): Promise<CheckResultSnapshot[]> {
416+
const internetAccount = this.clientStore.getInternetAccount(assemblyName)
417+
const { baseURL } = internetAccount
418+
const url = new URL('checks', baseURL)
419+
url.search = new URLSearchParams({ assembly: assemblyName }).toString()
420+
const response = await this.fetch(internetAccount, url.toString())
421+
if (!response.ok) {
422+
const errorMessage = await createFetchErrorMessage(
423+
response,
424+
'getCheckResults failed',
425+
)
426+
throw new Error(errorMessage)
427+
}
428+
return response.json() as Promise<CheckResultSnapshot[]>
429+
}
430+
415431
async submitChange(
416432
change: Change | AssemblySpecificChange,
417433
opts: SubmitOpts = {},

packages/jbrowse-plugin-apollo/src/BackendDrivers/LocalDriver/LocalDriver.ts

Lines changed: 111 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
type Change,
55
FeatureChange,
66
type SerializedChange,
7+
checkRegistry,
78
isFeatureChange,
89
} from '@apollo-annotation/common'
910
import type {
@@ -45,17 +46,28 @@ export class LocalDriver extends BackendDriver {
4546
const regions = await this.getRegions(assemblyName)
4647
const refNames = regions.map((r) => r.refName)
4748
const db = await openDb(assemblyName, refNames)
48-
const storeName = `features-${refName}`
49+
const featureStoreName = `features-${refName}`
50+
const checkStoreName = `checkresults-${refName}`
4951
const features: AnnotationFeatureSnapshot[] = []
50-
for await (const cursor of db
51-
.transaction(storeName)
52-
.store.index('min')
52+
const checkResults: CheckResultSnapshot[] = []
53+
const tx = db.transaction([featureStoreName, checkStoreName])
54+
for await (const cursor of tx
55+
.objectStore(featureStoreName)
56+
.index('min')
5357
.iterate(IDBKeyRange.upperBound(end, true))) {
5458
if ((cursor.value as { max: number }).max > start) {
5559
features.push(cursor.value as AnnotationFeatureSnapshot)
5660
}
5761
}
58-
return [features, []]
62+
for await (const cursor of tx
63+
.objectStore(checkStoreName)
64+
.index('min')
65+
.iterate(IDBKeyRange.upperBound(end, true))) {
66+
if ((cursor.value as { end: number }).end > start) {
67+
checkResults.push(cursor.value as CheckResultSnapshot)
68+
}
69+
}
70+
return [features, checkResults]
5971
}
6072

6173
async getSequence(region: Region): Promise<{ seq: string; refSeq: string }> {
@@ -150,10 +162,14 @@ export class LocalDriver extends BackendDriver {
150162
const regions = await this.getRegions(assembly)
151163
const refNames = regions.map((r) => r.refName)
152164
const db = await openDb(assembly, refNames)
153-
const storeNames = refNames.map((r) => `features-${r}`)
165+
const storeNames = refNames.flatMap((r) => [
166+
`features-${r}`,
167+
`checkresults-${r}`,
168+
])
154169
storeNames.push('changes')
155170
const tx = db.transaction(storeNames, 'readwrite')
156171
const topLevelFeatures = new Set<AnnotationFeature>()
172+
const deletedFeatureIds: { refSeq: string; featureId: string }[] = []
157173
if (isDeleteFeatureChange(change)) {
158174
for (const c of change.changes) {
159175
if (c.parentFeatureId) {
@@ -162,8 +178,9 @@ export class LocalDriver extends BackendDriver {
162178
topLevelFeatures.add(feature.topLevelFeature)
163179
}
164180
} else {
165-
const { refSeq } = c.deletedFeature
181+
const { refSeq, _id } = c.deletedFeature
166182
void tx.objectStore(`features-${refSeq}`).delete(c.deletedFeature._id)
183+
deletedFeatureIds.push({ refSeq, featureId: _id })
167184
}
168185
}
169186
} else {
@@ -180,10 +197,82 @@ export class LocalDriver extends BackendDriver {
180197
.objectStore(`features-${feature.refSeq}`)
181198
.put(snapshot, feature._id)
182199
}
200+
// Delete old check results for deleted features
201+
for (const { featureId, refSeq } of deletedFeatureIds) {
202+
const checkStore = tx.objectStore(`checkresults-${refSeq}`)
203+
for await (const cursor of checkStore
204+
.index('featureId')
205+
.iterate(featureId)) {
206+
this.clientStore.deleteCheckResult(
207+
(cursor.value as CheckResultSnapshot)._id,
208+
)
209+
void cursor.delete()
210+
}
211+
}
212+
// Delete old check results for modified features
213+
for (const feature of topLevelFeatures) {
214+
const checkStore = tx.objectStore(`checkresults-${feature.refSeq}`)
215+
for await (const cursor of checkStore
216+
.index('featureId')
217+
.iterate(feature._id)) {
218+
this.clientStore.deleteCheckResult(
219+
(cursor.value as CheckResultSnapshot)._id,
220+
)
221+
void cursor.delete()
222+
}
223+
}
183224
void tx
184225
.objectStore('changes')
185226
.put({ ...change.toJSON(), createdAt: new Date() })
186227
await tx.done
228+
229+
// Run checks on modified features. Collect all results first since checks
230+
// are async (need sequence data) and would cause the transaction to auto-commit.
231+
if (topLevelFeatures.size > 0) {
232+
const checks = [...checkRegistry.getChecks().values()]
233+
const allResults: {
234+
refSeq: string
235+
result: CheckResultSnapshot
236+
topLevelFeatureId: string
237+
}[] = []
238+
for (const feature of topLevelFeatures) {
239+
const snapshot = getSnapshot<AnnotationFeatureSnapshot>(feature)
240+
const getSequence = async (start: number, end: number) => {
241+
const result = await this.getSequence({
242+
assemblyName: assembly,
243+
refName: feature.refSeq,
244+
start,
245+
end,
246+
})
247+
return result.seq
248+
}
249+
for (const check of checks) {
250+
const results = await check.checkFeature(snapshot, getSequence)
251+
for (const result of results) {
252+
allResults.push({
253+
refSeq: feature.refSeq,
254+
result,
255+
topLevelFeatureId: feature._id,
256+
})
257+
}
258+
}
259+
}
260+
if (allResults.length > 0) {
261+
const checkStoreNames = refNames.map((r) => `checkresults-${r}`)
262+
const checkTx = db.transaction(checkStoreNames, 'readwrite')
263+
for (const { refSeq, result, topLevelFeatureId } of allResults) {
264+
void checkTx
265+
.objectStore(`checkresults-${refSeq}`)
266+
.put({ ...result, featureId: topLevelFeatureId })
267+
}
268+
await checkTx.done
269+
// Add new check results to client store
270+
for (const { result } of allResults) {
271+
this.clientStore.addCheckResult(result)
272+
}
273+
}
274+
}
275+
187276
return new ValidationResultSet()
188277
}
189278

@@ -194,6 +283,21 @@ export class LocalDriver extends BackendDriver {
194283
return []
195284
}
196285

286+
async getCheckResults(assemblyName: string): Promise<CheckResultSnapshot[]> {
287+
const regions = await this.getRegions(assemblyName)
288+
const refNames = regions.map((r) => r.refName)
289+
const db = await openDb(assemblyName, refNames)
290+
const checkResults: CheckResultSnapshot[] = []
291+
const storeNames = refNames.map((r) => `checkresults-${r}`)
292+
const tx = db.transaction(storeNames)
293+
for (const storeName of storeNames) {
294+
for await (const cursor of tx.objectStore(storeName).iterate()) {
295+
checkResults.push(cursor.value as CheckResultSnapshot)
296+
}
297+
}
298+
return checkResults
299+
}
300+
197301
async getChanges(assemblyName: string): Promise<ChangeDocument[]> {
198302
const regions = await this.getRegions(assemblyName)
199303
const refNames = regions.map((r) => r.refName)

packages/jbrowse-plugin-apollo/src/BackendDrivers/LocalDriver/db.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,17 @@ export async function openDb(
2020
store.createIndex('min', 'min', { unique: false })
2121
store.createIndex('max', 'max', { unique: false })
2222
}
23+
const checkStoreName = `checkresults-${refName}`
24+
if (!db.objectStoreNames.contains(checkStoreName)) {
25+
const store = db.createObjectStore(checkStoreName, {
26+
keyPath: '_id',
27+
})
28+
store.createIndex('min', 'start', { unique: false })
29+
store.createIndex('max', 'end', { unique: false })
30+
store.createIndex('featureId', 'featureId', {
31+
unique: false,
32+
})
33+
}
2334
}
2435
},
2536
})

packages/jbrowse-plugin-apollo/src/ChangeManager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,8 @@ export class ChangeManager {
138138
}
139139
console.error(error)
140140
session.notify(String(error), 'error')
141-
await this.undo(change, false)
142141
setChangeInProgress(false)
142+
await this.undo(change, false)
143143
return
144144
}
145145
if (!backendResult.ok) {
@@ -148,8 +148,8 @@ export class ChangeManager {
148148
jobsManager.abortJob(job.name, msg)
149149
}
150150
session.notify(msg, 'error')
151-
await this.undo(change, false)
152151
setChangeInProgress(false)
152+
await this.undo(change, false)
153153
return
154154
}
155155
if (change.notification) {

packages/jbrowse-plugin-apollo/src/LinearApolloDisplay/stateModel/base.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
types,
2828
} from '@jbrowse/mobx-state-tree'
2929
import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
30+
import FactCheckIcon from '@mui/icons-material/FactCheck'
3031
import TrackChangesIcon from '@mui/icons-material/TrackChanges'
3132
import { autorun } from 'mobx'
3233

@@ -37,6 +38,7 @@ import {
3738
} from '../../components/DownloadGFF3'
3839
import { FilterFeatures } from '../../components/FilterFeatures'
3940
import { ViewChangeLog } from '../../components/ViewChangeLog'
41+
import { ViewCheckResults } from '../../components/ViewCheckResults'
4042
import type { ApolloSessionModel, HoveredFeature } from '../../session'
4143
import type { ApolloRootModel } from '../../types'
4244
import { EditZoomThresholdDialog } from '../../util/displayUtils'
@@ -332,6 +334,31 @@ export function baseModelFactory(
332334
)
333335
},
334336
},
337+
{
338+
label: 'View Check Results',
339+
icon: FactCheckIcon,
340+
onClick: () => {
341+
const [region] = self.regions
342+
const { assemblyName } = region
343+
const assembly = self.getAssemblyId(assemblyName)
344+
if (!assembly) {
345+
return
346+
}
347+
const session = self.session as unknown as ApolloSessionModel
348+
;(session as unknown as AbstractSessionModel).queueDialog(
349+
(doneCallback) => [
350+
ViewCheckResults,
351+
{
352+
session,
353+
handleClose: () => {
354+
doneCallback()
355+
},
356+
assembly,
357+
},
358+
],
359+
)
360+
},
361+
},
335362
]
336363
},
337364
}

0 commit comments

Comments
 (0)