diff --git a/config/jest/url.js b/config/jest/url.js new file mode 100644 index 0000000000..4f9f954083 --- /dev/null +++ b/config/jest/url.js @@ -0,0 +1,2 @@ +URL.createObjectURL = () => `${Math.random()}` +URL.revokeObjectURL = () => {} diff --git a/packages/core/assemblyManager/assembly.ts b/packages/core/assemblyManager/assembly.ts index 3e6ff3b895..a4e987d43a 100644 --- a/packages/core/assemblyManager/assembly.ts +++ b/packages/core/assemblyManager/assembly.ts @@ -58,11 +58,10 @@ async function loadRefNameMap( assembly: Assembly, adapterConfig: unknown, options: BaseOptions, - signal?: AbortSignal, + stopToken?: string, ) { const { sessionId } = options await when(() => !!(assembly.regions && assembly.refNameAliases), { - signal, name: 'when assembly ready', }) @@ -71,7 +70,7 @@ async function loadRefNameMap( 'CoreGetRefNames', { adapterConfig, - signal, + stopToken, ...options, }, { timeout: 1000000 }, @@ -139,18 +138,14 @@ export default function assemblyFactory( cache: new QuickLRU({ maxSize: 1000 }), // @ts-expect-error + // TODO:ABORT (possible? desirable??) async fill( args: CacheData, - signal?: AbortSignal, + _stopToken?: string, statusCallback?: (arg: string) => void, ) { const { adapterConf, self, options } = args - return loadRefNameMap( - self, - adapterConf, - { ...options, statusCallback }, - signal, - ) + return loadRefNameMap(self, adapterConf, { ...options, statusCallback }) }, }) return types @@ -161,11 +156,29 @@ export default function assemblyFactory( configuration: types.safeReference(assemblyConfigType), }) .volatile(() => ({ + /** + * #volatile + */ error: undefined as unknown, + /** + * #volatile + */ loadingP: undefined as Promise | undefined, + /** + * #volatile + */ volatileRegions: undefined as BasicRegion[] | undefined, + /** + * #volatile + */ refNameAliases: undefined as RefNameAliases | undefined, + /** + * #volatile + */ lowerCaseRefNameAliases: undefined as RefNameAliases | undefined, + /** + * #volatile + */ cytobands: undefined as Feature[] | undefined, })) .views(self => ({ @@ -179,6 +192,8 @@ export default function assemblyFactory( .views(self => ({ /** * #getter + * this is a getter with a side effect of loading the data. not the best + * practice, but it helps to lazy load the assembly */ get initialized() { // @ts-expect-error @@ -216,7 +231,7 @@ export default function assemblyFactory( return self.getConf('displayName') }, /** - * #getter + * #method */ hasName(name: string) { return this.allAliases.includes(name) @@ -453,7 +468,7 @@ export default function assemblyFactory( * #method */ getAdapterMapEntry(adapterConf: AdapterConf, options: BaseOptions) { - const { signal, statusCallback, ...rest } = options + const { stopToken, statusCallback, ...rest } = options if (!options.sessionId) { throw new Error('sessionId is required') } @@ -465,7 +480,7 @@ export default function assemblyFactory( options: rest, } as CacheData, - // signal intentionally not passed here, fixes issues like #2221. + // stopToken intentionally not passed here, fixes issues like #2221. // alternative fix #2540 was proposed but non-working currently undefined, statusCallback, @@ -504,11 +519,11 @@ export default function assemblyFactory( async function getRefNameAliases({ config, pluginManager, - signal, + stopToken, }: { config: AnyConfigurationModel pluginManager: PluginManager - signal?: AbortSignal + stopToken?: string }) { const type = pluginManager.getAdapterType(config.type)! const CLASS = await type.getAdapterClass() @@ -517,7 +532,7 @@ async function getRefNameAliases({ undefined, pluginManager, ) as BaseRefNameAliasAdapter - return adapter.getRefNameAliases({ signal }) + return adapter.getRefNameAliases({ stopToken }) } async function getCytobands({ @@ -538,16 +553,16 @@ async function getCytobands({ async function getAssemblyRegions({ config, pluginManager, - signal, + stopToken, }: { config: AnyConfigurationModel pluginManager: PluginManager - signal?: AbortSignal + stopToken?: string }) { const type = pluginManager.getAdapterType(config.type)! const CLASS = await type.getAdapterClass() const adapter = new CLASS(config, undefined, pluginManager) as RegionsAdapter - return adapter.getRegions({ signal }) + return adapter.getRegions({ stopToken }) } export type AssemblyModel = ReturnType diff --git a/packages/core/assemblyManager/assemblyManager.ts b/packages/core/assemblyManager/assemblyManager.ts index 11a04830a0..1e2d3bafdd 100644 --- a/packages/core/assemblyManager/assemblyManager.ts +++ b/packages/core/assemblyManager/assemblyManager.ts @@ -121,7 +121,7 @@ function assemblyManagerFactory(conf: IAnyType, pm: PluginManager) { async getRefNameMapForAdapter( adapterConf: AdapterConf, assemblyName: string | undefined, - opts: { signal?: AbortSignal; sessionId: string }, + opts: { stopToken?: string; sessionId: string }, ) { if (assemblyName) { const asm = await this.waitForAssembly(assemblyName) @@ -136,7 +136,7 @@ function assemblyManagerFactory(conf: IAnyType, pm: PluginManager) { async getReverseRefNameMapForAdapter( adapterConf: AdapterConf, assemblyName: string | undefined, - opts: { signal?: AbortSignal; sessionId: string }, + opts: { stopToken?: string; sessionId: string }, ) { if (assemblyName) { const asm = await this.waitForAssembly(assemblyName) diff --git a/packages/core/data_adapters/BaseAdapter/BaseFeatureDataAdapter.ts b/packages/core/data_adapters/BaseAdapter/BaseFeatureDataAdapter.ts index 82bb6aa0da..9d189de860 100644 --- a/packages/core/data_adapters/BaseAdapter/BaseFeatureDataAdapter.ts +++ b/packages/core/data_adapters/BaseAdapter/BaseFeatureDataAdapter.ts @@ -6,10 +6,11 @@ import { BaseAdapter } from './BaseAdapter' import { BaseOptions } from './BaseOptions' import { FeatureDensityStats } from './types' import { ObservableCreate } from '../../util/rxjs' -import { checkAbortSignal, sum, max, min } from '../../util' +import { sum, max, min } from '../../util' import { Feature } from '../../util/simpleFeature' import { AugmentedRegion as Region } from '../../util/types' import { blankStats, rectifyStats, scoresToStats } from '../../util/stats' +import { checkStopToken } from '../../util/stopToken' /** * Base class for feature adapters to extend. Defines some methods that @@ -82,7 +83,7 @@ export abstract class BaseFeatureDataAdapter extends BaseAdapter { public getFeaturesInRegion(region: Region, opts: BaseOptions = {}) { return ObservableCreate(async observer => { const hasData = await this.hasDataForRefName(region.refName, opts) - checkAbortSignal(opts.signal) + checkStopToken(opts.stopToken) if (!hasData) { observer.complete() } else { diff --git a/packages/core/data_adapters/BaseAdapter/BaseOptions.ts b/packages/core/data_adapters/BaseAdapter/BaseOptions.ts index 759e096103..87989d48bf 100644 --- a/packages/core/data_adapters/BaseAdapter/BaseOptions.ts +++ b/packages/core/data_adapters/BaseAdapter/BaseOptions.ts @@ -1,5 +1,5 @@ export interface BaseOptions { - signal?: AbortSignal + stopToken?: string bpPerPx?: number sessionId?: string statusCallback?: (message: string) => void @@ -12,7 +12,7 @@ export type SearchType = 'full' | 'prefix' | 'exact' export interface BaseTextSearchArgs { queryString: string searchType?: SearchType - signal?: AbortSignal + stopToken?: string limit?: number pageNumber?: number } diff --git a/packages/core/data_adapters/BaseAdapter/types.ts b/packages/core/data_adapters/BaseAdapter/types.ts index 717c3a2dc5..e94363aabd 100644 --- a/packages/core/data_adapters/BaseAdapter/types.ts +++ b/packages/core/data_adapters/BaseAdapter/types.ts @@ -1,5 +1,5 @@ export interface BaseOptions { - signal?: AbortSignal + stopToken?: string bpPerPx?: number sessionId?: string statusCallback?: (message: string) => void @@ -12,7 +12,7 @@ export type SearchType = 'full' | 'prefix' | 'exact' export interface BaseTextSearchArgs { queryString: string searchType?: SearchType - signal?: AbortSignal + stopToken?: string limit?: number pageNumber?: number } diff --git a/packages/core/pluggableElementTypes/RpcMethodType.test.ts b/packages/core/pluggableElementTypes/RpcMethodType.test.ts index 543308995c..1fd1e3724d 100644 --- a/packages/core/pluggableElementTypes/RpcMethodType.test.ts +++ b/packages/core/pluggableElementTypes/RpcMethodType.test.ts @@ -31,7 +31,7 @@ test('test serialize arguments with augmentLocationObject', async () => { testLocation: locationInAdapter, }, filters: [], - signal: 'teststring', + stopToken: 'teststring', randomProperty: 'randomstring', parentObject: { nestedObject: { diff --git a/packages/core/pluggableElementTypes/RpcMethodType.ts b/packages/core/pluggableElementTypes/RpcMethodType.ts index 14e0c4e3db..9940ace9e6 100644 --- a/packages/core/pluggableElementTypes/RpcMethodType.ts +++ b/packages/core/pluggableElementTypes/RpcMethodType.ts @@ -10,16 +10,6 @@ import { UriLocation, } from '../util/types' -import { - deserializeAbortSignal, - isRemoteAbortSignal, - RemoteAbortSignal, -} from '../rpc/remoteAbortSignals' - -interface SerializedArgs { - signal?: RemoteAbortSignal - blobMap?: Record -} export type RpcMethodConstructor = new (pm: PluginManager) => RpcMethodType export default abstract class RpcMethodType extends PluggableElementBase { @@ -58,21 +48,15 @@ export default abstract class RpcMethodType extends PluggableElementBase { return loc } - async deserializeArguments( - serializedArgs: T, + async deserializeArguments( + args: T & { blobMap?: Record }, _rpcDriverClassName: string, - ) { - if (serializedArgs.blobMap) { - setBlobMap(serializedArgs.blobMap) + ): Promise { + if (args.blobMap) { + setBlobMap(args.blobMap) } - const { signal } = serializedArgs - return { - ...serializedArgs, - signal: isRemoteAbortSignal(signal) - ? deserializeAbortSignal(signal) - : undefined, - } + return args } abstract execute( diff --git a/packages/core/pluggableElementTypes/renderers/FeatureRendererType.ts b/packages/core/pluggableElementTypes/renderers/FeatureRendererType.ts index d1a62aeea9..b22324fec1 100644 --- a/packages/core/pluggableElementTypes/renderers/FeatureRendererType.ts +++ b/packages/core/pluggableElementTypes/renderers/FeatureRendererType.ts @@ -3,7 +3,7 @@ import clone from 'clone' import { firstValueFrom } from 'rxjs' // locals -import { checkAbortSignal, iterMap } from '../../util' +import { iterMap } from '../../util' import SimpleFeature, { Feature, SimpleFeatureSerialized, @@ -20,6 +20,7 @@ import ServerSideRendererType, { } from './ServerSideRendererType' import { isFeatureAdapter } from '../../data_adapters/BaseAdapter' import { AnyConfigurationModel } from '../../configuration' +import { checkStopToken } from '../../util/stopToken' export interface RenderArgs extends ServerSideRenderArgs { displayModel?: { @@ -146,7 +147,7 @@ export default class FeatureRendererType extends ServerSideRendererType { renderArgs: RenderArgsDeserialized, ): Promise> { const pm = this.pluginManager - const { signal, regions, sessionId, adapterConfig } = renderArgs + const { stopToken, regions, sessionId, adapterConfig } = renderArgs const { dataAdapter } = await getAdapter(pm, sessionId, adapterConfig) if (!isFeatureAdapter(dataAdapter)) { throw new Error('Adapter does not support retrieving features') @@ -176,7 +177,7 @@ export default class FeatureRendererType extends ServerSideRendererType { : dataAdapter.getFeaturesInMultipleRegions(requestRegions, renderArgs) const feats = await firstValueFrom(featureObservable.pipe(toArray())) - checkAbortSignal(signal) + checkStopToken(stopToken) return new Map( feats .filter(feat => this.featurePassesFilters(renderArgs, feat)) diff --git a/packages/core/pluggableElementTypes/renderers/ServerSideRendererType.tsx b/packages/core/pluggableElementTypes/renderers/ServerSideRendererType.tsx index 0df1817c9f..c10aa5472f 100644 --- a/packages/core/pluggableElementTypes/renderers/ServerSideRendererType.tsx +++ b/packages/core/pluggableElementTypes/renderers/ServerSideRendererType.tsx @@ -10,7 +10,7 @@ import { } from 'mobx-state-tree' // locals -import { checkAbortSignal, getSerializedSvg, updateStatus } from '../../util' +import { getSerializedSvg, updateStatus } from '../../util' import SerializableFilterChain, { SerializedFilterChain, } from './util/serializableFilterChain' @@ -20,12 +20,13 @@ import { createJBrowseTheme } from '../../ui' import RendererType, { RenderProps, RenderResults } from './RendererType' import ServerSideRenderedContent from './ServerSideRenderedContent' +import { checkStopToken } from '../../util/stopToken' interface BaseRenderArgs extends RenderProps { sessionId: string - // Note that signal serialization happens after serializeArgsInClient and + // Note that stopToken serialization happens after serializeArgsInClient and // deserialization happens before deserializeArgsInWorker - signal?: AbortSignal + stopToken?: string theme: ThemeOptions exportSVG?: { rasterizeLayers?: boolean @@ -189,13 +190,13 @@ export default class ServerSideRenderer extends RendererType { * @param args - serialized render args */ async renderInWorker(args: RenderArgsSerialized): Promise { - const { signal, statusCallback = () => {} } = args + const { stopToken, statusCallback = () => {} } = args const deserializedArgs = this.deserializeArgsInWorker(args) const results = await updateStatus('Rendering plot', statusCallback, () => this.render(deserializedArgs), ) - checkAbortSignal(signal) + checkStopToken(stopToken) // serialize the results for passing back to the main thread. // these will be transmitted to the main process, and will come out diff --git a/packages/core/rpc/BaseRpcDriver.test.ts b/packages/core/rpc/BaseRpcDriver.test.ts deleted file mode 100644 index 116e00c7aa..0000000000 --- a/packages/core/rpc/BaseRpcDriver.test.ts +++ /dev/null @@ -1,199 +0,0 @@ -import PluginManager from '../PluginManager' -import { checkAbortSignal } from '../util' -import BaseRpcDriver, { watchWorker, WorkerHandle } from './BaseRpcDriver' -import RpcMethodType from '../pluggableElementTypes/RpcMethodType' -import { ConfigurationSchema } from '../configuration' - -function delay(ms: number) { - return new Promise(resolve => { - setTimeout(() => { - resolve(true) - }, ms) - }) -} - -class MockWorkerHandle implements WorkerHandle { - busy = false - - destroy() {} - - async call( - name: string, - _args = [], - opts: { - timeout?: number - signal?: AbortSignal - rpcDriverClassName?: string - } = {}, - ) { - const { signal, timeout = 3000 } = opts - const start = Date.now() - switch (name) { - case 'ping': { - while (this.busy) { - if (timeout < +Date.now() - start) { - throw new Error('timeout') - } - - await delay(50) - } - - break - } - case 'doWorkShortPingTime': { - this.busy = true - await delay(50) - this.busy = false - await delay(50) - this.busy = true - await delay(50) - this.busy = false - await delay(50) - this.busy = true - await delay(50) - this.busy = false - - break - } - case 'doWorkLongPingTime': { - this.busy = true - await delay(1000) - checkAbortSignal(signal) - this.busy = false - await delay(1000) - checkAbortSignal(signal) - this.busy = true - await delay(1000) - checkAbortSignal(signal) - this.busy = false - - break - } - case 'MockRenderTimeout': { - this.busy = true - await delay(10000) - this.busy = false - - break - } - case 'MockRenderShort': { - this.busy = true - await delay(100) - checkAbortSignal(signal) - this.busy = false - - break - } - // No default - } - } -} -test('watch worker with long ping, generates timeout', async () => { - const worker = new MockWorkerHandle() - - expect.assertions(1) - try { - const workerWatcher = watchWorker(worker, 200, 'MockRpcDriver') - const result = worker.call('doWorkLongPingTime', undefined, { - timeout: 100, - rpcDriverClassName: 'MockRpcDriver', - }) - await Promise.race([result, workerWatcher]) - } catch (e) { - expect(`${e}`).toMatch(/timeout/) - } -}) - -test('test worker abort', async () => { - const worker = new MockWorkerHandle() - expect.assertions(1) - - try { - const controller = new AbortController() - const resultP = worker.call('doWorkLongPingTime', undefined, { - signal: controller.signal, - timeout: 2000, - rpcDriverClassName: 'MockRpcDriver', - }) - controller.abort() - await resultP - } catch (e) { - expect(`${e}`).toMatch(/abort/) - } -}) - -test('watch worker generates multiple pings', async () => { - const worker = new MockWorkerHandle() - const workerWatcher = watchWorker(worker, 200, 'MockRpcDriver') - const result = worker.call('doWorkShortPingTime') - await Promise.race([result, workerWatcher]) -}) - -class MockRpcDriver extends BaseRpcDriver { - name = 'MockRpcDriver' - - maxPingTime = 1000 - - workerCheckFrequency = 500 - - async makeWorker() { - return new MockWorkerHandle() - } -} - -class MockRendererTimeout extends RpcMethodType { - name = 'MockRenderTimeout' - - async execute() {} -} - -class MockRendererShort extends RpcMethodType { - name = 'MockRenderShort' - - async execute() {} -} - -test('test RPC driver operation timeout and worker replace', async () => { - const consoleMock = jest.spyOn(console, 'error').mockImplementation() - expect.assertions(1) - const config = ConfigurationSchema('Mock', {}).create() - const driver = new MockRpcDriver({ config }) - const pluginManager = new PluginManager() - - pluginManager.addRpcMethod(() => new MockRendererTimeout(pluginManager)) - pluginManager.addRpcMethod(() => new MockRendererShort(pluginManager)) - pluginManager.createPluggableElements() - try { - await driver.call(pluginManager, 'sessionId', 'MockRenderTimeout', {}, {}) - } catch (e) { - expect(`${e}`).toMatch(/operation timed out/) - } - await driver.call(pluginManager, 'sessionId', 'MockRenderShort', {}, {}) - consoleMock.mockRestore() -}) - -test('remote abort', async () => { - const consoleMock = jest.spyOn(console, 'error').mockImplementation() - expect.assertions(1) - const config = ConfigurationSchema('Mock', {}).create() - const driver = new MockRpcDriver({ config }) - const pluginManager = new PluginManager() - - pluginManager.addRpcMethod(() => new MockRendererShort(pluginManager)) - pluginManager.createPluggableElements() - try { - const controller = new AbortController() - const resP = driver.call( - pluginManager, - 'sessionId', - 'MockRenderShort', - {}, - { signal: controller.signal }, - ) - controller.abort() - await resP - } catch (e) { - expect(`${e}`).toMatch(/abort/) - } - consoleMock.mockRestore() -}) diff --git a/packages/core/rpc/BaseRpcDriver.ts b/packages/core/rpc/BaseRpcDriver.ts index c5adfd3add..2f246ca5a1 100644 --- a/packages/core/rpc/BaseRpcDriver.ts +++ b/packages/core/rpc/BaseRpcDriver.ts @@ -1,6 +1,5 @@ import { isAlive, isStateTreeNode } from 'mobx-state-tree' import { clamp } from '../util' -import { serializeAbortSignal } from './remoteAbortSignals' import PluginManager from '../PluginManager' import { readConfObject, AnyConfigurationModel } from '../configuration' @@ -94,7 +93,8 @@ export default abstract class BaseRpcDriver { private lastWorkerAssignment = -1 - private workerAssignments = new Map() // sessionId -> worker number + // sessionId -> worker number + private workerAssignments = new Map() abstract makeWorker(): Promise @@ -116,40 +116,32 @@ export default abstract class BaseRpcDriver { return thing .filter(thing => isCloneable(thing)) .map(t => this.filterArgs(t, sessionId)) as unknown as THING_TYPE - } - if (typeof thing === 'object' && thing !== null) { - // AbortSignals are specially handled - if (thing instanceof AbortSignal) { - return serializeAbortSignal( - thing, - this.remoteAbort.bind(this, sessionId), - ) as unknown as THING_TYPE - } - + } else if (typeof thing === 'object' && thing !== null) { if (isStateTreeNode(thing) && !isAlive(thing)) { throw new Error('dead state tree node passed to RPC call') - } - - // special case, don't try to iterate the file's subelements as the - // object entries below would - if (thing instanceof File) { + } else if (thing instanceof File) { return thing + } else { + return Object.fromEntries( + Object.entries(thing) + .filter(e => isCloneable(e[1])) + .map(([k, v]) => [k, this.filterArgs(v, sessionId)]), + ) as THING_TYPE } - - return Object.fromEntries( - Object.entries(thing) - .filter(e => isCloneable(e[1])) - .map(([k, v]) => [k, this.filterArgs(v, sessionId)]), - ) as THING_TYPE + } else { + return thing } - return thing } - async remoteAbort(sessionId: string, functionName: string, signalId: number) { + async remoteAbort( + sessionId: string, + functionName: string, + stopTokenId: number, + ) { const worker = await this.getWorker(sessionId) await worker.call( functionName, - { signalId }, + { stopTokenId }, { timeout: 1000000, rpcDriverClassName: this.name }, ) } diff --git a/packages/core/rpc/methods/CoreGetFeatureDensityStats.ts b/packages/core/rpc/methods/CoreGetFeatureDensityStats.ts index d2efdba8d6..9396a6bbb5 100644 --- a/packages/core/rpc/methods/CoreGetFeatureDensityStats.ts +++ b/packages/core/rpc/methods/CoreGetFeatureDensityStats.ts @@ -1,7 +1,6 @@ import { getAdapter } from '../../data_adapters/dataAdapterCache' import RpcMethodType from '../../pluggableElementTypes/RpcMethodType' import { RenderArgs } from './util' -import { RemoteAbortSignal } from '../remoteAbortSignals' import { isFeatureAdapter } from '../../data_adapters/BaseAdapter' import { renameRegionsIfNeeded, Region } from '../../util' @@ -10,7 +9,7 @@ export default class CoreGetFeatureDensityStats extends RpcMethodType { async serializeArguments( args: RenderArgs & { - signal?: AbortSignal + stopToken?: string statusCallback?: (arg: string) => void }, rpcDriver: string, @@ -29,7 +28,7 @@ export default class CoreGetFeatureDensityStats extends RpcMethodType { args: { adapterConfig: Record regions: Region[] - signal?: RemoteAbortSignal + stopToken?: string headers?: Record sessionId: string }, diff --git a/packages/core/rpc/methods/CoreGetFeatureDetails.ts b/packages/core/rpc/methods/CoreGetFeatureDetails.ts index 126ebe27e8..ef4f7a0eaf 100644 --- a/packages/core/rpc/methods/CoreGetFeatureDetails.ts +++ b/packages/core/rpc/methods/CoreGetFeatureDetails.ts @@ -1,6 +1,5 @@ import RpcMethodType from '../../pluggableElementTypes/RpcMethodType' import { RenderArgs } from './util' -import { RemoteAbortSignal } from '../remoteAbortSignals' import { renameRegionsIfNeeded, getLayoutId } from '../../util' import { RenderArgsSerialized } from './util' @@ -28,7 +27,7 @@ export default class CoreGetFeatureDetails extends RpcMethodType { } async execute( - args: RenderArgsSerialized & { signal?: RemoteAbortSignal }, + args: RenderArgsSerialized & { stopToken?: string }, rpcDriver: string, ) { let deserializedArgs = args diff --git a/packages/core/rpc/methods/CoreGetFeatures.ts b/packages/core/rpc/methods/CoreGetFeatures.ts index 4f93167aab..e5162c114d 100644 --- a/packages/core/rpc/methods/CoreGetFeatures.ts +++ b/packages/core/rpc/methods/CoreGetFeatures.ts @@ -5,7 +5,6 @@ import { firstValueFrom } from 'rxjs' import { getAdapter } from '../../data_adapters/dataAdapterCache' import RpcMethodType from '../../pluggableElementTypes/RpcMethodType' import { RenderArgs } from './util' -import { RemoteAbortSignal } from '../remoteAbortSignals' import { isFeatureAdapter } from '../../data_adapters/BaseAdapter' import { renameRegionsIfNeeded, Region } from '../../util' import SimpleFeature, { @@ -43,22 +42,22 @@ export default class CoreGetFeatures extends RpcMethodType { sessionId: string regions: Region[] adapterConfig: Record - signal?: RemoteAbortSignal - + stopToken?: string opts?: any }, rpcDriver: string, ) { const pm = this.pluginManager const deserializedArgs = await this.deserializeArguments(args, rpcDriver) - const { signal, sessionId, adapterConfig, regions, opts } = deserializedArgs + const { stopToken, sessionId, adapterConfig, regions, opts } = + deserializedArgs const { dataAdapter } = await getAdapter(pm, sessionId, adapterConfig) if (!isFeatureAdapter(dataAdapter)) { throw new Error('Adapter does not support retrieving features') } const ret = dataAdapter.getFeaturesInMultipleRegions(regions, { ...opts, - signal, + stopToken, }) const r = await firstValueFrom(ret.pipe(toArray())) return r.map(f => f.toJSON()) diff --git a/packages/core/rpc/methods/CoreGetFileInfo.ts b/packages/core/rpc/methods/CoreGetFileInfo.ts index dc7478b439..1be064c1fb 100644 --- a/packages/core/rpc/methods/CoreGetFileInfo.ts +++ b/packages/core/rpc/methods/CoreGetFileInfo.ts @@ -1,7 +1,5 @@ import { getAdapter } from '../../data_adapters/dataAdapterCache' import RpcMethodType from '../../pluggableElementTypes/RpcMethodType' - -import { RemoteAbortSignal } from '../remoteAbortSignals' import { isFeatureAdapter } from '../../data_adapters/BaseAdapter' export default class CoreGetFileInfo extends RpcMethodType { @@ -10,7 +8,7 @@ export default class CoreGetFileInfo extends RpcMethodType { async execute( args: { sessionId: string - signal: RemoteAbortSignal + stopToken?: string adapterConfig: Record }, rpcDriver: string, diff --git a/packages/core/rpc/methods/CoreGetMetadata.ts b/packages/core/rpc/methods/CoreGetMetadata.ts index 85a454e8d6..2f72fefae0 100644 --- a/packages/core/rpc/methods/CoreGetMetadata.ts +++ b/packages/core/rpc/methods/CoreGetMetadata.ts @@ -1,7 +1,5 @@ import { getAdapter } from '../../data_adapters/dataAdapterCache' import RpcMethodType from '../../pluggableElementTypes/RpcMethodType' - -import { RemoteAbortSignal } from '../remoteAbortSignals' import { isFeatureAdapter } from '../../data_adapters/BaseAdapter' export default class CoreGetMetadata extends RpcMethodType { @@ -10,7 +8,7 @@ export default class CoreGetMetadata extends RpcMethodType { async execute( args: { sessionId: string - signal: RemoteAbortSignal + stopToken?: string adapterConfig: Record }, rpcDriver: string, diff --git a/packages/core/rpc/methods/CoreGetRefNames.ts b/packages/core/rpc/methods/CoreGetRefNames.ts index e8b415b234..bb6f16df5c 100644 --- a/packages/core/rpc/methods/CoreGetRefNames.ts +++ b/packages/core/rpc/methods/CoreGetRefNames.ts @@ -1,7 +1,5 @@ import { getAdapter } from '../../data_adapters/dataAdapterCache' import RpcMethodType from '../../pluggableElementTypes/RpcMethodType' - -import { RemoteAbortSignal } from '../remoteAbortSignals' import { isFeatureAdapter } from '../../data_adapters/BaseAdapter' export default class CoreGetRefNames extends RpcMethodType { @@ -10,7 +8,7 @@ export default class CoreGetRefNames extends RpcMethodType { async execute( args: { sessionId: string - signal: RemoteAbortSignal + stopToken?: string adapterConfig: Record }, rpcDriver: string, diff --git a/packages/core/rpc/methods/CoreRender.ts b/packages/core/rpc/methods/CoreRender.ts index ea4154f6ce..ad5bc114a1 100644 --- a/packages/core/rpc/methods/CoreRender.ts +++ b/packages/core/rpc/methods/CoreRender.ts @@ -6,8 +6,8 @@ import { RenderArgsSerialized, validateRendererType, } from './util' -import { RemoteAbortSignal } from '../remoteAbortSignals' -import { checkAbortSignal, renameRegionsIfNeeded } from '../../util' +import { renameRegionsIfNeeded } from '../../util' +import { checkStopToken } from '../../util/stopToken' /** * fetches features from an adapter and call a renderer with them @@ -39,19 +39,19 @@ export default class CoreRender extends RpcMethodType { } async execute( - args: RenderArgsSerialized & { signal?: RemoteAbortSignal }, + args: RenderArgsSerialized & { stopToken?: string }, rpcDriver: string, ) { let deserializedArgs = args if (rpcDriver !== 'MainThreadRpcDriver') { deserializedArgs = await this.deserializeArguments(args, rpcDriver) } - const { sessionId, rendererType, signal } = deserializedArgs + const { sessionId, rendererType, stopToken } = deserializedArgs if (!sessionId) { throw new Error('must pass a unique session id') } - checkAbortSignal(signal) + checkStopToken(stopToken) const RendererType = validateRendererType( rendererType, @@ -63,7 +63,7 @@ export default class CoreRender extends RpcMethodType { ? await RendererType.render(deserializedArgs) : await RendererType.renderInWorker(deserializedArgs) - checkAbortSignal(signal) + checkStopToken(stopToken) return result } diff --git a/packages/core/rpc/remoteAbortSignals.ts b/packages/core/rpc/remoteAbortSignals.ts deleted file mode 100644 index db0ee3a1c3..0000000000 --- a/packages/core/rpc/remoteAbortSignals.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* ---------------- for the RPC client ----------------- */ - -let abortSignalCounter = 0 -export interface RemoteAbortSignal { - abortSignalId: number -} -const abortSignalIds = new WeakMap() // map of abortsignal => numerical ID - -/** - * assign an ID to the given abort signal and return a plain object - * representation - * - * @param signal - the signal to serialize - * @param callfunc - function used to call - * a remote method, will be called like callfunc('signalAbort', signalId) - */ -export function serializeAbortSignal( - signal: AbortSignal, - callfunc: (name: string, abortSignalId: number) => void, -): RemoteAbortSignal { - let abortSignalId = abortSignalIds.get(signal) - if (!abortSignalId) { - abortSignalCounter += 1 - abortSignalIds.set(signal, abortSignalCounter) - abortSignalId = abortSignalCounter - signal.addEventListener('abort', () => { - const signalId = abortSignalIds.get(signal) - if (signalId !== undefined) { - callfunc('signalAbort', signalId) - } - }) - } - return { abortSignalId } -} - -/* ---------------- for the RPC server ----------------- */ - -/** - * test whether a given object - * @param thing - the thing to test - * @returns true if the thing is a remote abort signal - */ -export function isRemoteAbortSignal( - thing: unknown, -): thing is RemoteAbortSignal { - return ( - typeof thing === 'object' && - thing !== null && - 'abortSignalId' in thing && - typeof thing.abortSignalId === 'number' - ) -} - -// the server side keeps a set of surrogate abort controllers that can be -// aborted based on ID -const surrogateAbortControllers = new Map() // numerical ID => surrogate abort controller - -/** - * deserialize the result of serializeAbortSignal into an AbortSignal - * - * @param signal - - * @returns an abort signal that corresponds to the given ID - */ -export function deserializeAbortSignal({ - abortSignalId, -}: RemoteAbortSignal): AbortSignal { - let surrogateAbortController = surrogateAbortControllers.get(abortSignalId) - if (!surrogateAbortController) { - surrogateAbortController = new AbortController() - surrogateAbortControllers.set(abortSignalId, surrogateAbortController) - } - return surrogateAbortController.signal -} - -/** - * fire an abort signal from a remote abort signal ID - * - * @param abortSignalId - - */ -export function remoteAbort(props: { signalId: number }) { - const { signalId: abortSignalId } = props - const surrogateAbortController = surrogateAbortControllers.get(abortSignalId) - - if (surrogateAbortController) { - surrogateAbortController.abort() - } -} - -export function remoteAbortRpcHandler() { - return { - signalAbort: remoteAbort, - } -} diff --git a/packages/core/util/aborting.ts b/packages/core/util/aborting.ts index 781a2cf3a4..8bdb49b884 100644 --- a/packages/core/util/aborting.ts +++ b/packages/core/util/aborting.ts @@ -5,10 +5,9 @@ class AbortError extends Error { } /** - * properly check if the given AbortSignal is aborted. - * per the standard, if the signal reads as aborted, - * this function throws either a DOMException AbortError, or a regular error - * with a `code` attribute set to `ERR_ABORTED`. + * properly check if the given AbortSignal is aborted. per the standard, if the + * signal reads as aborted, this function throws either a DOMException + * AbortError, or a regular error with a `code` attribute set to `ERR_ABORTED`. * * for convenience, passing `undefined` is a no-op * diff --git a/packages/core/util/index.ts b/packages/core/util/index.ts index c997b4c4a7..7cd7aa75f9 100644 --- a/packages/core/util/index.ts +++ b/packages/core/util/index.ts @@ -3,7 +3,6 @@ import isObject from 'is-object' import PluginManager from '../PluginManager' import type { Buffer } from 'buffer' import { - addDisposer, getParent, getSnapshot, getEnv as getEnvMST, @@ -14,7 +13,6 @@ import { IStateTreeNode, Instance, } from 'mobx-state-tree' -import { reaction, IReactionPublic, IReactionOptions } from 'mobx' import { Feature } from './simpleFeature' import { isSessionModel, @@ -26,7 +24,6 @@ import { TypeTestedByPredicate, } from './types' import type { Region as MUIRegion } from './types/mst' -import { isAbortException, checkAbortSignal } from './aborting' import { BaseBlock } from './blockTypes' import { isUriLocation } from './types' @@ -38,8 +35,8 @@ import { flushSync, render } from 'react-dom' import { GenericFilehandle } from 'generic-filehandle' import { unzip } from '@gmod/bgzf-filehandle' import { BaseOptions } from '../data_adapters/BaseAdapter' +import { checkStopToken } from './stopToken' export * from './types' -export * from './aborting' export * from './when' export * from './range' export * from './dedupe' @@ -690,102 +687,6 @@ export function findLast( return undefined } -/** - * makes a mobx reaction with the given functions, that calls actions on the - * model for each stage of execution, and to abort the reaction function when - * the model is destroyed. - * - * Will call startedFunction(signal), successFunction(result), and - * errorFunction(error) when the async reaction function starts, completes, and - * errors respectively. - * - * @param self - - * @param dataFunction - - * @param asyncReactionFunction - - * @param reactionOptions - - * @param startedFunction - - * @param successFunction - - * @param errorFunction - - */ -export function makeAbortableReaction( - self: T, - dataFunction: (arg: T) => U, - asyncReactionFunction: ( - arg: U | undefined, - signal: AbortSignal, - model: T, - handle: IReactionPublic, - ) => Promise, - // @ts-expect-error - reactionOptions: IReactionOptions, - startedFunction: (aborter: AbortController) => void, - successFunction: (arg: V) => void, - errorFunction: (err: unknown) => void, -) { - let inProgress: AbortController | undefined - - function handleError(error: unknown) { - if (!isAbortException(error)) { - if (isAlive(self)) { - errorFunction(error) - } else { - console.error(error) - } - } - } - - addDisposer( - self, - reaction( - () => { - try { - return dataFunction(self) - } catch (e) { - handleError(e) - return undefined - } - }, - async (data, mobxReactionHandle) => { - if (inProgress && !inProgress.signal.aborted) { - inProgress.abort() - } - - if (!isAlive(self)) { - return - } - inProgress = new AbortController() - - const thisInProgress = inProgress - startedFunction(thisInProgress) - try { - const result = await asyncReactionFunction( - data, - thisInProgress.signal, - self, - // @ts-expect-error - mobxReactionHandle, - ) - checkAbortSignal(thisInProgress.signal) - if (isAlive(self)) { - successFunction(result) - } - } catch (e) { - if (!thisInProgress.signal.aborted) { - thisInProgress.abort() - } - handleError(e) - } - }, - reactionOptions, - ), - ) - addDisposer(self, () => { - if (inProgress && !inProgress.signal.aborted) { - inProgress.abort() - } - }) -} - export function renameRegionIfNeeded( refNameMap: Record | undefined, region: Region | Instance, @@ -813,7 +714,7 @@ export async function renameRegionsIfNeeded< ARGTYPE extends { assemblyName?: string regions?: Region[] - signal?: AbortSignal + stopToken?: string adapterConfig: Record sessionId: string statusCallback?: (arg: string) => void @@ -1109,6 +1010,21 @@ export async function updateStatus( return res } +// call statusCallback with current status and clear when finished, and check +// stopToken afterwards +export async function updateStatus2( + msg: string, + cb: (arg: string) => void, + stopToken: string | undefined, + fn: () => U | Promise, +) { + cb(msg) + const res = await fn() + checkStopToken(stopToken) + cb('') + return res +} + export function hashCode(str: string) { let hash = 0 if (str.length === 0) { @@ -1505,3 +1421,4 @@ export { } from './simpleFeature' export { blobToDataURL } from './blobToDataURL' +export { makeAbortableReaction } from './makeAbortableReaction' diff --git a/packages/core/util/io/RemoteFileWithRangeCache.ts b/packages/core/util/io/RemoteFileWithRangeCache.ts index facab694e7..9442746d47 100644 --- a/packages/core/util/io/RemoteFileWithRangeCache.ts +++ b/packages/core/util/io/RemoteFileWithRangeCache.ts @@ -6,7 +6,7 @@ type BinaryRangeFetch = ( url: string, start: number, end: number, - options?: { headers?: HeadersInit; signal?: AbortSignal }, + options?: { headers?: HeadersInit; stopToken?: string }, ) => Promise export interface BinaryRangeResponse { @@ -22,7 +22,7 @@ function binaryRangeFetch( url: string, start: number, end: number, - options: { headers?: HeadersInit; signal?: AbortSignal } = {}, + options: { headers?: HeadersInit; stopToken?: string } = {}, ): Promise { const fetcher = fetchers[url] if (!fetcher) { @@ -61,11 +61,11 @@ export class RemoteFileWithRangeCache extends RemoteFile { const s = Number.parseInt(start!, 10) const e = Number.parseInt(end!, 10) const len = e - s + // tODO: abort const { buffer, headers } = (await globalRangeCache.getRange( url, s, len + 1, - { signal: init?.signal }, )) as BinaryRangeResponse return new Response(buffer, { status: 206, headers }) } @@ -77,7 +77,7 @@ export class RemoteFileWithRangeCache extends RemoteFile { url: string, start: number, end: number, - options: { headers?: HeadersInit; signal?: AbortSignal } = {}, + options: { headers?: HeadersInit; stopToken?: string } = {}, ): Promise { const requestDate = new Date() const res = await super.fetch(url, { diff --git a/packages/core/util/layouts/BaseLayout.ts b/packages/core/util/layouts/BaseLayout.ts index 3656221210..033d2e56e9 100644 --- a/packages/core/util/layouts/BaseLayout.ts +++ b/packages/core/util/layouts/BaseLayout.ts @@ -2,7 +2,7 @@ export type RectTuple = [number, number, number, number] export interface SerializedLayout { rectangles: Record totalHeight: number - containsNoTransferables: true + containsNoTransferables?: true maxHeightReached: boolean } export interface Rectangle { diff --git a/packages/core/util/makeAbortableReaction.ts b/packages/core/util/makeAbortableReaction.ts new file mode 100644 index 0000000000..5ce55cfe8a --- /dev/null +++ b/packages/core/util/makeAbortableReaction.ts @@ -0,0 +1,94 @@ +import { addDisposer, isAlive } from 'mobx-state-tree' +import { reaction, IReactionPublic, IReactionOptions } from 'mobx' +import { createStopToken, stopStopToken } from './stopToken' +import { isAbortException } from './aborting' + +/** + * makes a mobx reaction with the given functions, that calls actions on the + * model for each stage of execution, and to abort the reaction function when + * the model is destroyed. + * + * Will call startedFunction(stopToken), successFunction(result), and + * errorFunction(error) when the async reaction function starts, completes, and + * errors respectively. + * + * @param self - + * @param dataFunction - + * @param asyncReactionFunction - + * @param reactionOptions - + * @param startedFunction - + * @param successFunction - + * @param errorFunction - + */ +export function makeAbortableReaction( + self: T, + dataFunction: (arg: T) => U, + asyncReactionFunction: ( + arg: U | undefined, + stopToken: string, + model: T, + handle: IReactionPublic, + ) => Promise, + // @ts-expect-error + reactionOptions: IReactionOptions, + startedFunction: (stopToken: string) => void, + successFunction: (arg: V) => void, + errorFunction: (err: unknown) => void, +) { + let inProgress: string | undefined + + function handleError(error: unknown) { + if (!isAbortException(error)) { + console.error(error) + if (isAlive(self)) { + errorFunction(error) + } + } + } + + addDisposer( + self, + reaction( + () => { + try { + return dataFunction(self) + } catch (e) { + handleError(e) + return undefined + } + }, + async (data, mobxReactionHandle) => { + if (inProgress) { + stopStopToken(inProgress) + } + + if (!isAlive(self)) { + return + } + inProgress = createStopToken() + + startedFunction(inProgress) + try { + const result = await asyncReactionFunction( + data, + inProgress, + self, + // @ts-expect-error + mobxReactionHandle, + ) + if (isAlive(self)) { + successFunction(result) + } + } catch (e) { + handleError(e) + } + }, + reactionOptions, + ), + ) + addDisposer(self, () => { + if (inProgress) { + stopStopToken(inProgress) + } + }) +} diff --git a/packages/core/util/nextTick.ts b/packages/core/util/nextTick.ts new file mode 100644 index 0000000000..585e7f34c3 --- /dev/null +++ b/packages/core/util/nextTick.ts @@ -0,0 +1,5 @@ +export default function nextTickMod() { + return new Promise( + resolve => requestAnimationFrame(resolve) || setTimeout(resolve, 1), + ) +} diff --git a/packages/core/util/rxjs.ts b/packages/core/util/rxjs.ts index 04309595dd..0028aa91ef 100644 --- a/packages/core/util/rxjs.ts +++ b/packages/core/util/rxjs.ts @@ -1,15 +1,14 @@ import { Observable, Observer } from 'rxjs' -import { takeUntil } from 'rxjs/operators' -import { observeAbortSignal } from '.' /** * Wrapper for rxjs Observable.create with improved error handling and * aborting support * @param func - observer function, could be async + * TODO:ABORTING? */ export function ObservableCreate( func: (arg: Observer) => void | Promise, - signal?: AbortSignal, + _stopToken?: string, ): Observable { return new Observable((observer: Observer) => { try { @@ -22,5 +21,5 @@ export function ObservableCreate( } catch (error) { observer.error(error) } - }).pipe(takeUntil(observeAbortSignal(signal))) + }) } diff --git a/packages/core/util/stopToken.ts b/packages/core/util/stopToken.ts new file mode 100644 index 0000000000..92c6cbb951 --- /dev/null +++ b/packages/core/util/stopToken.ts @@ -0,0 +1,55 @@ +/** + * source https://github.com/panstromek/zebra-rs/blob/82d616225930b3ad423a2c6d883c79b94ee08ba6/webzebra/src/stopToken.ts#L34C1-L57C16 + * + * blogpost https://yoyo-code.com/how-to-stop-synchronous-web-worker/ + * + * license "I explicitly added MIT license to the stopToken file to make it more + * permissive" + * + * Copyright (c) 2022 Matyáš Racek + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +export function createStopToken() { + // URL not available in jest and can't properly mock it + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + return URL.createObjectURL?.(new Blob()) || `${Math.random()}` +} + +export function stopStopToken(stopToken: string) { + // URL not available in jest and can't properly mock it + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + URL.revokeObjectURL?.(stopToken) +} + +export function checkStopToken(stopToken?: string) { + if (typeof jest === 'undefined' && stopToken !== undefined) { + const xhr = new XMLHttpRequest() + + // synchronous XHR usage to check the token + xhr.open('GET', stopToken, false) + try { + xhr.send(null) + } catch (e) { + throw new Error('aborted') + } + } +} diff --git a/packages/core/util/tracks.ts b/packages/core/util/tracks.ts index b8850cfc6d..d8b769108a 100644 --- a/packages/core/util/tracks.ts +++ b/packages/core/util/tracks.ts @@ -85,6 +85,7 @@ export function getBlobMap() { return blobMap } +// TODO:IS THIS BAD? // used in new contexts like webworkers export function setBlobMap(map: Record) { blobMap = map diff --git a/packages/core/util/when.ts b/packages/core/util/when.ts index 27e975cec3..e7c01625db 100644 --- a/packages/core/util/when.ts +++ b/packages/core/util/when.ts @@ -1,88 +1 @@ -import { when as mobxWhen, IWhenOptions } from 'mobx' -import { makeAbortError } from './aborting' - -interface WhenOpts extends IWhenOptions { - signal?: AbortSignal -} - -/** - * Wrapper for mobx `when` that adds timeout and aborting support. - */ -export function when( - getter: () => boolean, - { timeout, signal, name }: WhenOpts = {}, -) { - return new Promise((resolve, reject) => { - let finished = false - - const whenPromise = mobxWhen(getter) - - // set up timeout - let timeoutId: ReturnType | undefined - let finishTimeout = () => {} - if (timeout) { - timeoutId = setTimeout(() => { - if (!finished) { - finished = true - whenPromise.cancel() - reject(new Error(`timed out waiting for ${name || 'whenPresent'}`)) - } - }, timeout) - finishTimeout = () => { - if (timeoutId) { - clearTimeout(timeoutId) - } - } - } - - // set up aborting - if (signal) { - signal.addEventListener('abort', () => { - if (!finished) { - finished = true - - // mobx when supports a cancel method - whenPromise.cancel() - finishTimeout() - - reject(makeAbortError()) - } - }) - } - - whenPromise - .then(() => { - if (!finished) { - finished = true - finishTimeout() - - resolve(true) - } - }) - .catch((err: unknown) => { - if (!finished) { - finished = true - finishTimeout() - // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors - reject(err) - } - }) - }) -} - -/** - * Wrapper for mobx `when` that makes a promise for the return value - * of the given function at the point in time when it becomes not - * undefined and not null. - */ -export async function whenPresent unknown>( - getter: FUNCTION, - opts: WhenOpts = {}, -): Promise>> { - await when(() => { - const val = getter() - return val !== undefined && val !== null - }, opts) - - return getter() as NonNullable> -} +export { when } from 'mobx' diff --git a/packages/product-core/src/rpcWorker.ts b/packages/product-core/src/rpcWorker.ts index bff52da481..a8bc05532e 100644 --- a/packages/product-core/src/rpcWorker.ts +++ b/packages/product-core/src/rpcWorker.ts @@ -1,6 +1,5 @@ import RpcServer from 'librpc-web-mod' import PluginManager from '@jbrowse/core/PluginManager' -import { remoteAbortRpcHandler } from '@jbrowse/core/rpc/remoteAbortSignals' import PluginLoader, { LoadedPlugin, PluginDefinition, @@ -91,7 +90,6 @@ export async function initializeWorker( // @ts-expect-error self.rpcServer = new RpcServer.Server({ ...rpcConfig, - ...remoteAbortRpcHandler(), ping: async () => { // the ping method is required by the worker driver for checking the // health of the worker diff --git a/packages/text-indexing/src/TextIndexing.ts b/packages/text-indexing/src/TextIndexing.ts index 25d940ab9c..498078c2b6 100644 --- a/packages/text-indexing/src/TextIndexing.ts +++ b/packages/text-indexing/src/TextIndexing.ts @@ -1,20 +1,20 @@ import fs from 'fs' import path from 'path' import { Readable } from 'stream' +import { isSupportedIndexingAdapter } from '@jbrowse/core/util' +import { checkStopToken } from '@jbrowse/core/util/stopToken' + +// misc import { indexGff3 } from './types/gff3Adapter' import { indexVcf } from './types/vcfAdapter' import { generateMeta } from './types/common' import { ixIxxStream } from 'ixixx' import { Track, indexType } from './util' -import { - checkAbortSignal, - isSupportedIndexingAdapter, -} from '@jbrowse/core/util' export async function indexTracks(args: { tracks: Track[] outDir?: string - signal?: AbortSignal + stopToken?: string attributesToIndex?: string[] assemblyNames?: string[] featureTypesToExclude?: string[] @@ -29,10 +29,10 @@ export async function indexTracks(args: { assemblyNames, indexType, statusCallback, - signal, + stopToken, } = args const idxType = indexType || 'perTrack' - checkAbortSignal(signal) + checkStopToken(stopToken) await (idxType === 'perTrack' ? perTrackIndex({ tracks, @@ -40,7 +40,7 @@ export async function indexTracks(args: { outDir, attributesToIndex, featureTypesToExclude, - signal, + stopToken, }) : aggregateIndex({ tracks, @@ -49,9 +49,9 @@ export async function indexTracks(args: { attributesToIndex, assemblyNames, featureTypesToExclude, - signal, + stopToken, })) - checkAbortSignal(signal) + checkStopToken(stopToken) return [] } @@ -61,14 +61,14 @@ async function perTrackIndex({ outDir: paramOutDir, attributesToIndex = ['Name', 'ID'], featureTypesToExclude = ['exon', 'CDS'], - signal, + stopToken, }: { tracks: Track[] statusCallback: (message: string) => void outDir?: string attributesToIndex?: string[] featureTypesToExclude?: string[] - signal?: AbortSignal + stopToken?: string }) { const outFlag = paramOutDir || '.' @@ -96,7 +96,7 @@ async function perTrackIndex({ featureTypesToExclude, assemblyNames, statusCallback, - signal, + stopToken, }) } } @@ -107,7 +107,7 @@ async function aggregateIndex({ outDir: paramOutDir, attributesToIndex = ['Name', 'ID'], featureTypesToExclude = ['exon', 'CDS'], - signal, + stopToken, assemblyNames, }: { tracks: Track[] @@ -116,7 +116,7 @@ async function aggregateIndex({ attributesToIndex?: string[] assemblyNames?: string[] featureTypesToExclude?: string[] - signal?: AbortSignal + stopToken?: string }) { const outFlag = paramOutDir || '.' const isDir = fs.lstatSync(outFlag).isDirectory() @@ -145,7 +145,7 @@ async function aggregateIndex({ featureTypesToExclude, assemblyNames: [asm], statusCallback, - signal, + stopToken, }) } } @@ -158,7 +158,7 @@ async function indexDriver({ featureTypesToExclude, assemblyNames, statusCallback, - signal, + stopToken, }: { tracks: Track[] outDir: string @@ -167,7 +167,7 @@ async function indexDriver({ featureTypesToExclude: string[] assemblyNames: string[] statusCallback: (message: string) => void - signal?: AbortSignal + stopToken?: string }) { const readable = Readable.from( indexFiles({ @@ -176,12 +176,12 @@ async function indexDriver({ outDir, featureTypesToExclude, statusCallback, - signal, + stopToken, }), ) statusCallback('Indexing files.') await runIxIxx(readable, outDir, name) - checkAbortSignal(signal) + checkStopToken(stopToken) await generateMeta({ configs: tracks, attributesToIndex, @@ -190,7 +190,7 @@ async function indexDriver({ featureTypesToExclude, assemblyNames, }) - checkAbortSignal(signal) + checkStopToken(stopToken) } async function* indexFiles({ @@ -205,7 +205,7 @@ async function* indexFiles({ outDir: string featureTypesToExclude: string[] statusCallback: (message: string) => void - signal?: AbortSignal + stopToken?: string }) { for (const track of tracks) { const { adapter, textSearching } = track diff --git a/plugins/alignments/src/AlignmentsFeatureDetail/tagInfo.ts b/plugins/alignments/src/AlignmentsFeatureDetail/tagInfo.ts index e668521114..c135fe5d59 100644 --- a/plugins/alignments/src/AlignmentsFeatureDetail/tagInfo.ts +++ b/plugins/alignments/src/AlignmentsFeatureDetail/tagInfo.ts @@ -17,7 +17,7 @@ export const tags = { E2: 'The 2nd most likely base calls', FI: 'The index of segment in the template', FS: 'Segment suffix', - FZ: 'Flow signal intensities', + FZ: 'Flow stopToken intensities', GC: 'Reserved for backwards compatibility reasons', GQ: 'Reserved for backwards compatibility reasons', GS: 'Reserved for backwards compatibility reasons', diff --git a/plugins/alignments/src/BamAdapter/BamAdapter.ts b/plugins/alignments/src/BamAdapter/BamAdapter.ts index 05a97b2f9e..83fa8ade57 100644 --- a/plugins/alignments/src/BamAdapter/BamAdapter.ts +++ b/plugins/alignments/src/BamAdapter/BamAdapter.ts @@ -15,6 +15,7 @@ import QuickLRU from '@jbrowse/core/util/QuickLRU' // locals import BamSlightlyLazyFeature from './BamSlightlyLazyFeature' import { FilterBy } from '../shared/types' +import { checkStopToken } from '@jbrowse/core/util/stopToken' interface Header { idToName: string[] @@ -75,9 +76,9 @@ export default class BamAdapter extends BaseFeatureDataAdapter { return this.configureP } - async getHeader(opts?: BaseOptions) { + async getHeader(_opts?: BaseOptions) { const { bam } = await this.configure() - return bam.getHeaderText(opts) + return bam.getHeaderText() } private async setupPre(opts?: BaseOptions) { @@ -87,7 +88,7 @@ export default class BamAdapter extends BaseFeatureDataAdapter { 'Downloading index', statusCallback, async () => { - const samHeader = await bam.getHeader(opts) + const samHeader = await bam.getHeader() // use the @SQ lines in the header to figure out the // mapping between ref ref ID numbers and names @@ -177,15 +178,17 @@ export default class BamAdapter extends BaseFeatureDataAdapter { }, ) { const { refName, start, end, originalRefName } = region - const { signal, filterBy, statusCallback = () => {} } = opts || {} + const { stopToken, filterBy, statusCallback = () => {} } = opts || {} return ObservableCreate(async observer => { const { bam } = await this.configure() await this.setup(opts) + checkStopToken(stopToken) const records = await updateStatus( 'Downloading alignments', statusCallback, - () => bam.getRecordsForRange(refName, start, end, opts), + () => bam.getRecordsForRange(refName, start, end), ) + checkStopToken(stopToken) await updateStatus('Processing alignments', statusCallback, async () => { const { @@ -240,7 +243,7 @@ export default class BamAdapter extends BaseFeatureDataAdapter { } observer.complete() }) - }, signal) + }) } async getMultiRegionFeatureDensityStats( diff --git a/plugins/alignments/src/CramAdapter/CramAdapter.ts b/plugins/alignments/src/CramAdapter/CramAdapter.ts index c9132e4eed..704bf4e22c 100644 --- a/plugins/alignments/src/CramAdapter/CramAdapter.ts +++ b/plugins/alignments/src/CramAdapter/CramAdapter.ts @@ -8,7 +8,7 @@ import { BaseSequenceAdapter, } from '@jbrowse/core/data_adapters/BaseAdapter' import type { Region, Feature } from '@jbrowse/core/util' -import { checkAbortSignal, updateStatus, toLocale } from '@jbrowse/core/util' +import { updateStatus, toLocale } from '@jbrowse/core/util' import { openLocation } from '@jbrowse/core/util/io' import { ObservableCreate } from '@jbrowse/core/util/rxjs' import QuickLRU from '@jbrowse/core/util/QuickLRU' @@ -16,6 +16,7 @@ import QuickLRU from '@jbrowse/core/util/QuickLRU' // locals import CramSlightlyLazyFeature from './CramSlightlyLazyFeature' import { FilterBy } from '../shared/types' +import { checkStopToken } from '@jbrowse/core/util/stopToken' interface Header { idToName?: string[] @@ -222,7 +223,7 @@ export default class CramAdapter extends BaseFeatureDataAdapter { filterBy: FilterBy }, ) { - const { signal, filterBy, statusCallback = () => {} } = opts || {} + const { stopToken, filterBy, statusCallback = () => {} } = opts || {} const { refName, start, end, originalRefName } = region return ObservableCreate(async observer => { @@ -243,7 +244,7 @@ export default class CramAdapter extends BaseFeatureDataAdapter { statusCallback, () => cram.getRecordsForRange(refId, start, end), ) - checkAbortSignal(signal) + checkStopToken(stopToken) await updateStatus('Processing alignments', statusCallback, () => { const { flagInclude = 0, @@ -289,7 +290,7 @@ export default class CramAdapter extends BaseFeatureDataAdapter { observer.complete() }) - }, signal) + }, stopToken) } freeResources(/* { region } */): void {} diff --git a/plugins/alignments/src/PileupRPC/base.ts b/plugins/alignments/src/PileupRPC/base.ts index a9a9fbe082..e1531963cb 100644 --- a/plugins/alignments/src/PileupRPC/base.ts +++ b/plugins/alignments/src/PileupRPC/base.ts @@ -6,7 +6,7 @@ import { RenderArgs } from '@jbrowse/core/rpc/coreRpcMethods' export default abstract class PileupBaseRPC extends RpcMethodType { async serializeArguments( args: RenderArgs & { - signal?: AbortSignal + stopToken?: string statusCallback?: (arg: string) => void }, rpcDriver: string, diff --git a/plugins/alignments/src/PileupRPC/methods/GetGlobalValueForTag.ts b/plugins/alignments/src/PileupRPC/methods/GetGlobalValueForTag.ts index 35fe622a71..76a654bb8f 100644 --- a/plugins/alignments/src/PileupRPC/methods/GetGlobalValueForTag.ts +++ b/plugins/alignments/src/PileupRPC/methods/GetGlobalValueForTag.ts @@ -1,6 +1,5 @@ import { getAdapter } from '@jbrowse/core/data_adapters/dataAdapterCache' import { Region } from '@jbrowse/core/util' -import { RemoteAbortSignal } from '@jbrowse/core/rpc/remoteAbortSignals' import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter' import { toArray } from 'rxjs/operators' import { firstValueFrom } from 'rxjs' @@ -14,7 +13,7 @@ export default class PileupGetGlobalValueForTag extends PileupBaseRPC { async execute( args: { adapterConfig: Record - signal?: RemoteAbortSignal + stopToken?: string headers?: Record regions: Region[] sessionId: string diff --git a/plugins/alignments/src/PileupRPC/methods/GetReducedFeatures.ts b/plugins/alignments/src/PileupRPC/methods/GetReducedFeatures.ts index d46f432051..631246b1b3 100644 --- a/plugins/alignments/src/PileupRPC/methods/GetReducedFeatures.ts +++ b/plugins/alignments/src/PileupRPC/methods/GetReducedFeatures.ts @@ -1,6 +1,5 @@ import { getAdapter } from '@jbrowse/core/data_adapters/dataAdapterCache' import { Region, dedupe, groupBy } from '@jbrowse/core/util' -import { RemoteAbortSignal } from '@jbrowse/core/rpc/remoteAbortSignals' import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter' import { toArray } from 'rxjs/operators' import { firstValueFrom } from 'rxjs' @@ -16,7 +15,7 @@ export default class PileupGetReducedFeatures extends PileupBaseRPC { async execute( args: { adapterConfig: Record - signal?: RemoteAbortSignal + stopToken?: string headers?: Record regions: Region[] sessionId: string diff --git a/plugins/alignments/src/PileupRPC/methods/GetVisibleModifications.ts b/plugins/alignments/src/PileupRPC/methods/GetVisibleModifications.ts index 7fdbc15056..8384bd6793 100644 --- a/plugins/alignments/src/PileupRPC/methods/GetVisibleModifications.ts +++ b/plugins/alignments/src/PileupRPC/methods/GetVisibleModifications.ts @@ -1,5 +1,4 @@ import { getAdapter } from '@jbrowse/core/data_adapters/dataAdapterCache' -import { RemoteAbortSignal } from '@jbrowse/core/rpc/remoteAbortSignals' import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter' import { Region } from '@jbrowse/core/util' import { toArray } from 'rxjs/operators' @@ -17,7 +16,7 @@ export default class PileupGetVisibleModifications extends PileupBaseRPC { async execute( args: { adapterConfig: Record - signal?: RemoteAbortSignal + stopToken?: string headers?: Record regions: Region[] sessionId: string diff --git a/plugins/alignments/src/PileupRenderer/PileupRenderer.ts b/plugins/alignments/src/PileupRenderer/PileupRenderer.ts index 3d0b510671..dd50966986 100644 --- a/plugins/alignments/src/PileupRenderer/PileupRenderer.ts +++ b/plugins/alignments/src/PileupRenderer/PileupRenderer.ts @@ -134,6 +134,7 @@ export default class PileupRenderer extends BoxRendererType { height, width, maxHeightReached: layout.maxHeightReached, + containsNoTransferables: true, } } diff --git a/plugins/alignments/src/PileupRenderer/makeImageData.ts b/plugins/alignments/src/PileupRenderer/makeImageData.ts index dddf5d5258..67089466c8 100644 --- a/plugins/alignments/src/PileupRenderer/makeImageData.ts +++ b/plugins/alignments/src/PileupRenderer/makeImageData.ts @@ -2,6 +2,7 @@ import { Feature } from '@jbrowse/core/util' import { RenderArgsDeserializedWithFeaturesAndLayout } from './PileupRenderer' import { readConfObject } from '@jbrowse/core/configuration' import { createJBrowseTheme } from '@jbrowse/core/ui' +import { checkStopToken } from '@jbrowse/core/util/stopToken' import { getCharWidthHeight, getColorBaseMap, @@ -32,7 +33,13 @@ export function makeImageData({ layoutRecords: LayoutFeature[] renderArgs: RenderArgsWithColor }) { - const { config, showSoftClip, colorBy, theme: configTheme } = renderArgs + const { + stopToken, + config, + showSoftClip, + colorBy, + theme: configTheme, + } = renderArgs const mismatchAlpha = readConfObject(config, 'mismatchAlpha') const minSubfeatureWidth = readConfObject(config, 'minSubfeatureWidth') const largeInsertionIndicatorScale = readConfObject( @@ -48,7 +55,12 @@ export function makeImageData({ const { charWidth, charHeight } = getCharWidthHeight() const drawSNPsMuted = shouldDrawSNPsMuted(colorBy?.type) const drawIndels = shouldDrawIndels() + let start = performance.now() for (const feat of layoutRecords) { + if (performance.now() - start > 400) { + checkStopToken(stopToken) + start = performance.now() + } renderAlignment({ ctx, feat, diff --git a/plugins/alignments/src/SNPCoverageAdapter/SNPCoverageAdapter.ts b/plugins/alignments/src/SNPCoverageAdapter/SNPCoverageAdapter.ts index 44c575bb36..915d2f1966 100644 --- a/plugins/alignments/src/SNPCoverageAdapter/SNPCoverageAdapter.ts +++ b/plugins/alignments/src/SNPCoverageAdapter/SNPCoverageAdapter.ts @@ -90,7 +90,7 @@ export default class SNPCoverageAdapter extends BaseFeatureDataAdapter { }) observer.complete() - }, opts.signal) + }, opts.stopToken) } async getMultiRegionFeatureDensityStats( diff --git a/plugins/alignments/src/SNPCoverageAdapter/generateCoverageBins.ts b/plugins/alignments/src/SNPCoverageAdapter/generateCoverageBins.ts index 95e6cfe0b7..756230736a 100644 --- a/plugins/alignments/src/SNPCoverageAdapter/generateCoverageBins.ts +++ b/plugins/alignments/src/SNPCoverageAdapter/generateCoverageBins.ts @@ -1,347 +1,14 @@ import { AugmentedRegion as Region } from '@jbrowse/core/util/types' -import { doesIntersect2, Feature, max, sum } from '@jbrowse/core/util' +import { Feature, sum } from '@jbrowse/core/util' +import { checkStopToken } from '@jbrowse/core/util/stopToken' // locals -import { parseCigar } from '../MismatchParser' -import { getMethBins } from '../ModificationParser' -import { - ColorBy, - Mismatch, - PreBaseCoverageBin, - PreBaseCoverageBinSubtypes, - SkipMap, -} from '../shared/types' -import { getMaxProbModAtEachPosition } from '../shared/getMaximumModificationAtEachPosition' - -function mismatchLen(mismatch: Mismatch) { - return !isInterbase(mismatch.type) ? mismatch.length : 1 -} - -function isInterbase(type: string) { - return type === 'softclip' || type === 'hardclip' || type === 'insertion' -} - -function inc( - bin: PreBaseCoverageBin, - strand: -1 | 0 | 1, - type: keyof PreBaseCoverageBinSubtypes, - field: string, -) { - let thisBin = bin[type][field] - if (thisBin === undefined) { - thisBin = bin[type][field] = { - entryDepth: 0, - probabilities: [], - '-1': 0, - '0': 0, - '1': 0, - } - } - thisBin.entryDepth++ - thisBin[strand]++ -} - -interface Opts { - bpPerPx?: number - colorBy?: ColorBy -} - -function processDepth({ - feature, - bins, - region, - regionSequence, -}: { - feature: Feature - bins: PreBaseCoverageBin[] - region: Region - regionSequence: string -}) { - const fstart = feature.get('start') - const fend = feature.get('end') - const fstrand = feature.get('strand') as -1 | 0 | 1 - const regionLength = region.end - region.start - for (let j = fstart; j < fend + 1; j++) { - const i = j - region.start - if (i >= 0 && i < regionLength) { - if (bins[i] === undefined) { - bins[i] = { - depth: 0, - readsCounted: 0, - refbase: regionSequence[i], - ref: { - probabilities: [], - entryDepth: 0, - '-1': 0, - 0: 0, - 1: 0, - }, - snps: {}, - mods: {}, - nonmods: {}, - delskips: {}, - noncov: {}, - } - } - if (j !== fend) { - bins[i].depth++ - bins[i].readsCounted++ - bins[i].ref.entryDepth++ - bins[i].ref[fstrand]++ - } - } - } -} -function processSNPs({ - feature, - region, - bins, - skipmap, -}: { - region: Region - bins: PreBaseCoverageBin[] - feature: Feature - skipmap: SkipMap -}) { - const fstart = feature.get('start') - const fstrand = feature.get('strand') as -1 | 0 | 1 - const mismatches = (feature.get('mismatches') as Mismatch[] | undefined) ?? [] - - // normal SNP based coloring - for (const mismatch of mismatches) { - const mstart = fstart + mismatch.start - const mlen = mismatchLen(mismatch) - const mend = mstart + mlen - for (let j = mstart; j < mstart + mlen; j++) { - const epos = j - region.start - if (epos >= 0 && epos < bins.length) { - const bin = bins[epos]! - const { base, type } = mismatch - const interbase = isInterbase(type) - - if (type === 'deletion' || type === 'skip') { - inc(bin, fstrand, 'delskips', type) - bin.depth-- - } else if (!interbase) { - inc(bin, fstrand, 'snps', base) - bin.ref.entryDepth-- - bin.ref[fstrand]-- - } else { - inc(bin, fstrand, 'noncov', type) - } - } - } - - if (mismatch.type === 'skip') { - // for upper case XS and TS: reports the literal strand of the genomic - // transcript - const tags = feature.get('tags') - const xs = tags?.XS || tags?.TS - // for lower case ts from minimap2: genomic transcript flipped by read - // strand - const ts = tags?.ts - const effectiveStrand = - xs === '+' - ? 1 - : xs === '-' - ? -1 - : (ts === '+' ? 1 : xs === '-' ? -1 : 0) * fstrand - const hash = `${mstart}_${mend}_${effectiveStrand}` - if (skipmap[hash] === undefined) { - skipmap[hash] = { - feature: feature, - start: mstart, - end: mend, - strand: fstrand, - effectiveStrand, - score: 0, - } - } - skipmap[hash].score++ - } - } -} - -function processReferenceCpGs({ - feature, - region, - bins, - regionSequence, -}: { - bins: PreBaseCoverageBin[] - feature: Feature - region: Region - regionSequence: string -}) { - const fstart = feature.get('start') - const fend = feature.get('end') - const fstrand = feature.get('strand') as -1 | 0 | 1 - const seq = feature.get('seq') as string | undefined - const mismatches = (feature.get('mismatches') as Mismatch[] | undefined) ?? [] - const r = regionSequence.toLowerCase() - if (seq) { - const cigarOps = parseCigar(feature.get('CIGAR')) - const { methBins, methProbs } = getMethBins(feature, cigarOps) - const dels = mismatches.filter(f => f.type === 'deletion') - - // methylation based coloring takes into account both reference sequence - // CpG detection and reads - for (let i = 0; i < fend - fstart; i++) { - const j = i + fstart - const l1 = r[j - region.start + 1] - const l2 = r[j - region.start + 2] - if (l1 === 'c' && l2 === 'g') { - const bin0 = bins[j - region.start] - const bin1 = bins[j - region.start + 1] - const b0 = methBins[i] - const b1 = methBins[i + 1] - const p0 = methProbs[i] - const p1 = methProbs[i + 1] - - // color - if ( - (b0 && (p0 !== undefined ? p0 > 0.5 : true)) || - (b1 && (p1 !== undefined ? p1 > 0.5 : true)) - ) { - if (bin0) { - incWithProbabilities(bin0, fstrand, 'mods', 'cpg_meth', p0 || 0) - bin0.ref.entryDepth-- - bin0.ref[fstrand]-- - } - if (bin1) { - incWithProbabilities(bin1, fstrand, 'mods', 'cpg_meth', p1 || 0) - bin1.ref.entryDepth-- - bin1.ref[fstrand]-- - } - } else { - if (bin0) { - if ( - !dels.some(d => - doesIntersect2( - j, - j + 1, - d.start + fstart, - d.start + fstart + d.length, - ), - ) - ) { - incWithProbabilities( - bin0, - fstrand, - 'nonmods', - 'cpg_unmeth', - 1 - (p0 || 0), - ) - bin0.ref.entryDepth-- - bin0.ref[fstrand]-- - } - } - if (bin1) { - if ( - !dels.some(d => - doesIntersect2( - j + 1, - j + 2, - d.start + fstart, - d.start + fstart + d.length, - ), - ) - ) { - incWithProbabilities( - bin1, - fstrand, - 'nonmods', - 'cpg_unmeth', - 1 - (p1 || 0), - ) - bin1.ref.entryDepth-- - bin1.ref[fstrand]-- - } - } - } - } - } - } -} - -function processModification({ - feature, - colorBy, - region, - bins, - regionSequence, -}: { - bins: PreBaseCoverageBin[] - feature: Feature - region: Region - colorBy?: ColorBy - regionSequence: string -}) { - const fstart = feature.get('start') - const fstrand = feature.get('strand') as -1 | 0 | 1 - const fend = feature.get('end') - const twoColor = colorBy?.modifications?.twoColor - const isolatedModification = colorBy?.modifications?.isolatedModification - getMaxProbModAtEachPosition(feature)?.forEach( - ({ type, prob, allProbs }, pos) => { - if (isolatedModification && type !== isolatedModification) { - return - } - const epos = pos + fstart - region.start - if (epos >= 0 && epos < bins.length && pos + fstart < fend) { - if (bins[epos] === undefined) { - bins[epos] = { - depth: 0, - readsCounted: 0, - refbase: regionSequence[epos], - snps: {}, - ref: { - probabilities: [], - entryDepth: 0, - '-1': 0, - 0: 0, - 1: 0, - }, - mods: {}, - nonmods: {}, - delskips: {}, - noncov: {}, - } - } - - const s = 1 - sum(allProbs) - const bin = bins[epos] - if (twoColor && s > max(allProbs)) { - incWithProbabilities(bin, fstrand, 'nonmods', `nonmod_${type}`, s) - } else { - incWithProbabilities(bin, fstrand, 'mods', `mod_${type}`, prob) - } - } - }, - ) -} - -function incWithProbabilities( - bin: PreBaseCoverageBin, - strand: -1 | 0 | 1, - type: keyof PreBaseCoverageBinSubtypes, - field: string, - probability: number, -) { - let thisBin = bin[type][field] - if (thisBin === undefined) { - thisBin = bin[type][field] = { - entryDepth: 0, - probabilities: [], - '-1': 0, - '0': 0, - '1': 0, - } - } - thisBin.entryDepth++ - thisBin.probabilities.push(probability) - thisBin[strand]++ -} +import { PreBaseCoverageBin, SkipMap } from '../shared/types' +import { processReferenceCpGs } from './processReferenceCpGs' +import { processModifications } from './processModifications' +import { processDepth } from './processDepth' +import { processMismatches } from './processMismatches' +import { Opts } from './util' export async function generateCoverageBins({ fetchSequence, @@ -354,7 +21,7 @@ export async function generateCoverageBins({ opts: Opts fetchSequence: (arg: Region) => Promise }) { - const { colorBy } = opts + const { stopToken, colorBy } = opts const skipmap = {} as SkipMap const bins = [] as PreBaseCoverageBin[] const start2 = Math.max(0, region.start - 1) @@ -365,7 +32,12 @@ export async function generateCoverageBins({ start: start2, end: region.end + 1, })) || '' + let start = performance.now() for (const feature of features) { + if (performance.now() - start > 400) { + checkStopToken(stopToken) + start = performance.now() + } processDepth({ feature, bins, @@ -374,7 +46,7 @@ export async function generateCoverageBins({ }) if (colorBy?.type === 'modifications') { - processModification({ + processModifications({ feature, colorBy, bins, @@ -389,7 +61,7 @@ export async function generateCoverageBins({ regionSequence, }) } - processSNPs({ feature, skipmap, bins, region }) + processMismatches({ feature, skipmap, bins, region }) } for (const bin of bins) { diff --git a/plugins/alignments/src/SNPCoverageAdapter/processDepth.ts b/plugins/alignments/src/SNPCoverageAdapter/processDepth.ts new file mode 100644 index 0000000000..11b2f73565 --- /dev/null +++ b/plugins/alignments/src/SNPCoverageAdapter/processDepth.ts @@ -0,0 +1,52 @@ +import { AugmentedRegion as Region } from '@jbrowse/core/util/types' +import { Feature } from '@jbrowse/core/util' + +// locals +import { PreBaseCoverageBin } from '../shared/types' + +export function processDepth({ + feature, + bins, + region, + regionSequence, +}: { + feature: Feature + bins: PreBaseCoverageBin[] + region: Region + regionSequence: string +}) { + const fstart = feature.get('start') + const fend = feature.get('end') + const fstrand = feature.get('strand') as -1 | 0 | 1 + const regionLength = region.end - region.start + for (let j = fstart; j < fend + 1; j++) { + const i = j - region.start + if (i >= 0 && i < regionLength) { + if (bins[i] === undefined) { + bins[i] = { + depth: 0, + readsCounted: 0, + refbase: regionSequence[i], + ref: { + probabilities: [], + entryDepth: 0, + '-1': 0, + 0: 0, + 1: 0, + }, + snps: {}, + mods: {}, + nonmods: {}, + delskips: {}, + noncov: {}, + } + } + if (j !== fend) { + bins[i].depth++ + bins[i].readsCounted++ + bins[i].ref.entryDepth++ + bins[i].ref[fstrand]++ + } + } + } +} diff --git a/plugins/alignments/src/SNPCoverageAdapter/processMismatches.ts b/plugins/alignments/src/SNPCoverageAdapter/processMismatches.ts new file mode 100644 index 0000000000..863534bfa7 --- /dev/null +++ b/plugins/alignments/src/SNPCoverageAdapter/processMismatches.ts @@ -0,0 +1,76 @@ +import { AugmentedRegion as Region } from '@jbrowse/core/util/types' +import { Feature } from '@jbrowse/core/util' + +// locals +import { Mismatch, PreBaseCoverageBin, SkipMap } from '../shared/types' +import { inc, isInterbase, mismatchLen } from './util' + +export function processMismatches({ + feature, + region, + bins, + skipmap, +}: { + region: Region + bins: PreBaseCoverageBin[] + feature: Feature + skipmap: SkipMap +}) { + const fstart = feature.get('start') + const fstrand = feature.get('strand') as -1 | 0 | 1 + const mismatches = (feature.get('mismatches') as Mismatch[] | undefined) ?? [] + + // normal SNP based coloring + for (const mismatch of mismatches) { + const mstart = fstart + mismatch.start + const mlen = mismatchLen(mismatch) + const mend = mstart + mlen + for (let j = mstart; j < mstart + mlen; j++) { + const epos = j - region.start + if (epos >= 0 && epos < bins.length) { + const bin = bins[epos]! + const { base, type } = mismatch + const interbase = isInterbase(type) + + if (type === 'deletion' || type === 'skip') { + inc(bin, fstrand, 'delskips', type) + bin.depth-- + } else if (!interbase) { + inc(bin, fstrand, 'snps', base) + bin.ref.entryDepth-- + bin.ref[fstrand]-- + } else { + inc(bin, fstrand, 'noncov', type) + } + } + } + + if (mismatch.type === 'skip') { + // for upper case XS and TS: reports the literal strand of the genomic + // transcript + const tags = feature.get('tags') + const xs = tags?.XS || tags?.TS + // for lower case ts from minimap2: genomic transcript flipped by read + // strand + const ts = tags?.ts + const effectiveStrand = + xs === '+' + ? 1 + : xs === '-' + ? -1 + : (ts === '+' ? 1 : xs === '-' ? -1 : 0) * fstrand + const hash = `${mstart}_${mend}_${effectiveStrand}` + if (skipmap[hash] === undefined) { + skipmap[hash] = { + feature: feature, + start: mstart, + end: mend, + strand: fstrand, + effectiveStrand, + score: 0, + } + } + skipmap[hash].score++ + } + } +} diff --git a/plugins/alignments/src/SNPCoverageAdapter/processModifications.ts b/plugins/alignments/src/SNPCoverageAdapter/processModifications.ts new file mode 100644 index 0000000000..7900b93c32 --- /dev/null +++ b/plugins/alignments/src/SNPCoverageAdapter/processModifications.ts @@ -0,0 +1,64 @@ +import { AugmentedRegion as Region } from '@jbrowse/core/util/types' +import { Feature, max, sum } from '@jbrowse/core/util' + +// locals +import { ColorBy, PreBaseCoverageBin } from '../shared/types' +import { getMaxProbModAtEachPosition } from '../shared/getMaximumModificationAtEachPosition' +import { incWithProbabilities } from './util' + +export function processModifications({ + feature, + colorBy, + region, + bins, + regionSequence, +}: { + bins: PreBaseCoverageBin[] + feature: Feature + region: Region + colorBy?: ColorBy + regionSequence: string +}) { + const fstart = feature.get('start') + const fstrand = feature.get('strand') as -1 | 0 | 1 + const fend = feature.get('end') + const twoColor = colorBy?.modifications?.twoColor + const isolatedModification = colorBy?.modifications?.isolatedModification + getMaxProbModAtEachPosition(feature)?.forEach( + ({ type, prob, allProbs }, pos) => { + if (isolatedModification && type !== isolatedModification) { + return + } + const epos = pos + fstart - region.start + if (epos >= 0 && epos < bins.length && pos + fstart < fend) { + if (bins[epos] === undefined) { + bins[epos] = { + depth: 0, + readsCounted: 0, + refbase: regionSequence[epos], + snps: {}, + ref: { + probabilities: [], + entryDepth: 0, + '-1': 0, + 0: 0, + 1: 0, + }, + mods: {}, + nonmods: {}, + delskips: {}, + noncov: {}, + } + } + + const s = 1 - sum(allProbs) + const bin = bins[epos] + if (twoColor && s > max(allProbs)) { + incWithProbabilities(bin, fstrand, 'nonmods', `nonmod_${type}`, s) + } else { + incWithProbabilities(bin, fstrand, 'mods', `mod_${type}`, prob) + } + } + }, + ) +} diff --git a/plugins/alignments/src/SNPCoverageAdapter/processReferenceCpGs.ts b/plugins/alignments/src/SNPCoverageAdapter/processReferenceCpGs.ts new file mode 100644 index 0000000000..9063595ade --- /dev/null +++ b/plugins/alignments/src/SNPCoverageAdapter/processReferenceCpGs.ts @@ -0,0 +1,110 @@ +import { AugmentedRegion as Region } from '@jbrowse/core/util/types' +import { doesIntersect2, Feature } from '@jbrowse/core/util' + +// locals +import { parseCigar } from '../MismatchParser' +import { getMethBins } from '../ModificationParser' +import { Mismatch, PreBaseCoverageBin } from '../shared/types' +import { incWithProbabilities } from './util' + +export function processReferenceCpGs({ + feature, + region, + bins, + regionSequence, +}: { + bins: PreBaseCoverageBin[] + feature: Feature + region: Region + regionSequence: string +}) { + const fstart = feature.get('start') + const fend = feature.get('end') + const fstrand = feature.get('strand') as -1 | 0 | 1 + const seq = feature.get('seq') as string | undefined + const mismatches = (feature.get('mismatches') as Mismatch[] | undefined) ?? [] + const r = regionSequence.toLowerCase() + if (seq) { + const cigarOps = parseCigar(feature.get('CIGAR')) + const { methBins, methProbs } = getMethBins(feature, cigarOps) + const dels = mismatches.filter(f => f.type === 'deletion') + + // methylation based coloring takes into account both reference sequence + // CpG detection and reads + for (let i = 0; i < fend - fstart; i++) { + const j = i + fstart + const l1 = r[j - region.start + 1] + const l2 = r[j - region.start + 2] + if (l1 === 'c' && l2 === 'g') { + const bin0 = bins[j - region.start] + const bin1 = bins[j - region.start + 1] + const b0 = methBins[i] + const b1 = methBins[i + 1] + const p0 = methProbs[i] + const p1 = methProbs[i + 1] + + // color + if ( + (b0 && (p0 !== undefined ? p0 > 0.5 : true)) || + (b1 && (p1 !== undefined ? p1 > 0.5 : true)) + ) { + if (bin0) { + incWithProbabilities(bin0, fstrand, 'mods', 'cpg_meth', p0 || 0) + bin0.ref.entryDepth-- + bin0.ref[fstrand]-- + } + if (bin1) { + incWithProbabilities(bin1, fstrand, 'mods', 'cpg_meth', p1 || 0) + bin1.ref.entryDepth-- + bin1.ref[fstrand]-- + } + } else { + if (bin0) { + if ( + !dels.some(d => + doesIntersect2( + j, + j + 1, + d.start + fstart, + d.start + fstart + d.length, + ), + ) + ) { + incWithProbabilities( + bin0, + fstrand, + 'nonmods', + 'cpg_unmeth', + 1 - (p0 || 0), + ) + bin0.ref.entryDepth-- + bin0.ref[fstrand]-- + } + } + if (bin1) { + if ( + !dels.some(d => + doesIntersect2( + j + 1, + j + 2, + d.start + fstart, + d.start + fstart + d.length, + ), + ) + ) { + incWithProbabilities( + bin1, + fstrand, + 'nonmods', + 'cpg_unmeth', + 1 - (p1 || 0), + ) + bin1.ref.entryDepth-- + bin1.ref[fstrand]-- + } + } + } + } + } + } +} diff --git a/plugins/alignments/src/SNPCoverageAdapter/util.ts b/plugins/alignments/src/SNPCoverageAdapter/util.ts new file mode 100644 index 0000000000..c080fb9397 --- /dev/null +++ b/plugins/alignments/src/SNPCoverageAdapter/util.ts @@ -0,0 +1,62 @@ +import { + ColorBy, + Mismatch, + PreBaseCoverageBin, + PreBaseCoverageBinSubtypes, +} from '../shared/types' + +export interface Opts { + bpPerPx?: number + colorBy?: ColorBy + stopToken?: string +} + +export function mismatchLen(mismatch: Mismatch) { + return !isInterbase(mismatch.type) ? mismatch.length : 1 +} + +export function isInterbase(type: string) { + return type === 'softclip' || type === 'hardclip' || type === 'insertion' +} + +export function inc( + bin: PreBaseCoverageBin, + strand: -1 | 0 | 1, + type: keyof PreBaseCoverageBinSubtypes, + field: string, +) { + let thisBin = bin[type][field] + if (thisBin === undefined) { + thisBin = bin[type][field] = { + entryDepth: 0, + probabilities: [], + '-1': 0, + '0': 0, + '1': 0, + } + } + thisBin.entryDepth++ + thisBin[strand]++ +} + +export function incWithProbabilities( + bin: PreBaseCoverageBin, + strand: -1 | 0 | 1, + type: keyof PreBaseCoverageBinSubtypes, + field: string, + probability: number, +) { + let thisBin = bin[type][field] + if (thisBin === undefined) { + thisBin = bin[type][field] = { + entryDepth: 0, + probabilities: [], + '-1': 0, + '0': 0, + '1': 0, + } + } + thisBin.entryDepth++ + thisBin.probabilities.push(probability) + thisBin[strand]++ +} diff --git a/plugins/alignments/src/SNPCoverageRenderer/SNPCoverageRenderer.ts b/plugins/alignments/src/SNPCoverageRenderer/SNPCoverageRenderer.ts index 5c934fed4a..ddd96c43d5 100644 --- a/plugins/alignments/src/SNPCoverageRenderer/SNPCoverageRenderer.ts +++ b/plugins/alignments/src/SNPCoverageRenderer/SNPCoverageRenderer.ts @@ -9,6 +9,7 @@ import { WiggleBaseRenderer, YSCALEBAR_LABEL_OFFSET, } from '@jbrowse/plugin-wiggle' +import { checkStopToken } from '@jbrowse/core/util/stopToken' // locals import { BaseCoverageBin, ModificationTypeWithColor } from '../shared/types' @@ -65,6 +66,7 @@ export default class SNPCoverageRenderer extends WiggleBaseRenderer { theme: configTheme, config: cfg, ticks, + stopToken, } = props const theme = createJBrowseTheme(configTheme) const region = regions[0]! @@ -120,6 +122,7 @@ export default class SNPCoverageRenderer extends WiggleBaseRenderer { // Use two pass rendering, which helps in visualizing the SNPs at higher // bpPerPx First pass: draw the gray background ctx.fillStyle = colorForBase.total! + let start = performance.now() for (const feature of feats) { if (feature.get('type') === 'skip') { continue @@ -128,6 +131,10 @@ export default class SNPCoverageRenderer extends WiggleBaseRenderer { const w = rightPx - leftPx + fudgeFactor const score = feature.get('score') as number ctx.fillRect(leftPx, toY(score), w, toHeight(score)) + if (performance.now() - start > 400) { + checkStopToken(stopToken) + start = performance.now() + } } // Keep track of previous total which we will use it to draw the interbase @@ -154,7 +161,12 @@ export default class SNPCoverageRenderer extends WiggleBaseRenderer { // Second pass: draw the SNP data, and add a minimum feature width of 1px // which can be wider than the actual bpPerPx This reduces overdrawing of // the grey background over the SNPs + start = performance.now() for (const feature of feats) { + const now = performance.now() + if (now - start > 400) { + checkStopToken(stopToken) + } if (feature.get('type') === 'skip') { continue } diff --git a/plugins/alignments/src/shared/getUniqueModifications.ts b/plugins/alignments/src/shared/getUniqueModifications.ts index 458813bdef..fcba8397c0 100644 --- a/plugins/alignments/src/shared/getUniqueModifications.ts +++ b/plugins/alignments/src/shared/getUniqueModifications.ts @@ -16,7 +16,7 @@ export async function getUniqueModifications({ blocks: BlockSet opts?: { headers?: Record - signal?: AbortSignal + stopToken?: string filters: string[] } }) { diff --git a/plugins/alignments/src/shared/getUniqueTags.ts b/plugins/alignments/src/shared/getUniqueTags.ts index 9914924011..7eff9a5c28 100644 --- a/plugins/alignments/src/shared/getUniqueTags.ts +++ b/plugins/alignments/src/shared/getUniqueTags.ts @@ -15,7 +15,7 @@ export async function getUniqueTags({ blocks: BlockSet opts?: { headers?: Record - signal?: AbortSignal + stopToken?: string filters: string[] } }) { diff --git a/plugins/bed/src/BedAdapter/BedAdapter.ts b/plugins/bed/src/BedAdapter/BedAdapter.ts index 8fb2dba8d9..f14f7fb519 100644 --- a/plugins/bed/src/BedAdapter/BedAdapter.ts +++ b/plugins/bed/src/BedAdapter/BedAdapter.ts @@ -170,7 +170,7 @@ export default class BedAdapter extends BaseFeatureDataAdapter { observer.next(f) }) observer.complete() - }, opts.signal) + }, opts.stopToken) } public freeResources(): void {} diff --git a/plugins/bed/src/BedTabixAdapter/BedTabixAdapter.ts b/plugins/bed/src/BedTabixAdapter/BedTabixAdapter.ts index 86b119bafc..11d8dbbe3f 100644 --- a/plugins/bed/src/BedTabixAdapter/BedTabixAdapter.ts +++ b/plugins/bed/src/BedTabixAdapter/BedTabixAdapter.ts @@ -101,10 +101,10 @@ export default class BedTabixAdapter extends BaseFeatureDataAdapter { ), ) }, - signal: opts.signal, + stopToken: opts.stopToken, }) observer.complete() - }, opts.signal) + }, opts.stopToken) } public freeResources(): void {} diff --git a/plugins/bed/src/BedpeAdapter/BedpeAdapter.ts b/plugins/bed/src/BedpeAdapter/BedpeAdapter.ts index a24b89e317..694f73fd3e 100644 --- a/plugins/bed/src/BedpeAdapter/BedpeAdapter.ts +++ b/plugins/bed/src/BedpeAdapter/BedpeAdapter.ts @@ -204,7 +204,7 @@ export default class BedpeAdapter extends BaseFeatureDataAdapter { observer.next(f) }) observer.complete() - }, opts.signal) + }, opts.stopToken) } public freeResources(): void {} diff --git a/plugins/bed/src/BigBedAdapter/BigBedAdapter.ts b/plugins/bed/src/BigBedAdapter/BigBedAdapter.ts index 0490c2fffd..63292069d9 100644 --- a/plugins/bed/src/BigBedAdapter/BigBedAdapter.ts +++ b/plugins/bed/src/BigBedAdapter/BigBedAdapter.ts @@ -102,7 +102,7 @@ export default class BigBedAdapter extends BaseFeatureDataAdapter { allowRedispatch: boolean originalQuery?: Region }) { - const { signal } = opts + const { stopToken } = opts const scoreColumn = this.getConf('scoreColumn') const aggregateField = this.getConf('aggregateField') const { parser, bigbed } = await this.configure(opts) @@ -111,7 +111,7 @@ export default class BigBedAdapter extends BaseFeatureDataAdapter { query.start, query.end, { - signal, + stopToken, basesPerSpan: query.end - query.start, }, ) @@ -234,7 +234,7 @@ export default class BigBedAdapter extends BaseFeatureDataAdapter { } catch (e) { observer.error(e) } - }, opts.signal) + }, opts.stopToken) } public freeResources(): void {} diff --git a/plugins/bed/src/generateBedMethylFeature.ts b/plugins/bed/src/generateBedMethylFeature.ts index 960e0abad3..db7faec2c9 100644 --- a/plugins/bed/src/generateBedMethylFeature.ts +++ b/plugins/bed/src/generateBedMethylFeature.ts @@ -44,13 +44,14 @@ export function generateBedMethylFeature({ n_diff, n_nocall, ] = line.split('\t') + return { uniqueId, refName, start, end, code, - score: fraction_modified, + score: +fraction_modified! || 0, strand, color, source: code, diff --git a/plugins/circular-view/src/BaseChordDisplay/models/model.tsx b/plugins/circular-view/src/BaseChordDisplay/models/model.tsx index 75d20eb1f5..942290e123 100644 --- a/plugins/circular-view/src/BaseChordDisplay/models/model.tsx +++ b/plugins/circular-view/src/BaseChordDisplay/models/model.tsx @@ -252,11 +252,11 @@ export const BaseChordDisplayModel = types assemblyManager: getSession(self).assemblyManager, }), - async ({ assemblyNames, adapter, assemblyManager }: any, signal) => { + async ({ assemblyNames, adapter, assemblyManager }: any, stopToken) => { return assemblyManager.getRefNameMapForAdapter( adapter, assemblyNames[0], - { signal, sessionId: getRpcSessionId(self) }, + { stopToken, sessionId: getRpcSessionId(self) }, ) }, { diff --git a/plugins/circular-view/src/BaseChordDisplay/models/renderReaction.ts b/plugins/circular-view/src/BaseChordDisplay/models/renderReaction.ts index 7edf9e3925..f6ddf0b281 100644 --- a/plugins/circular-view/src/BaseChordDisplay/models/renderReaction.ts +++ b/plugins/circular-view/src/BaseChordDisplay/models/renderReaction.ts @@ -26,7 +26,7 @@ export function renderReactionData(self: any) { export async function renderReactionEffect( props: any, - signal: AbortSignal | undefined, + stopToken: string | undefined, self: any, ) { if (!props) { diff --git a/plugins/comparative-adapters/src/PairwiseIndexedPAFAdapter/PairwiseIndexedPAFAdapter.ts b/plugins/comparative-adapters/src/PairwiseIndexedPAFAdapter/PairwiseIndexedPAFAdapter.ts index 22bd8c0da7..c7f487ec5b 100644 --- a/plugins/comparative-adapters/src/PairwiseIndexedPAFAdapter/PairwiseIndexedPAFAdapter.ts +++ b/plugins/comparative-adapters/src/PairwiseIndexedPAFAdapter/PairwiseIndexedPAFAdapter.ts @@ -124,7 +124,7 @@ export default class PAFAdapter extends BaseFeatureDataAdapter { }), ) }, - signal: opts.signal, + stopToken: opts.stopToken, }) observer.complete() diff --git a/plugins/dotplot-view/src/ComparativeRenderer/index.ts b/plugins/dotplot-view/src/ComparativeRenderer/index.ts index 2eb41dea87..3be4229b96 100644 --- a/plugins/dotplot-view/src/ComparativeRenderer/index.ts +++ b/plugins/dotplot-view/src/ComparativeRenderer/index.ts @@ -1,4 +1,3 @@ -import { checkAbortSignal } from '@jbrowse/core/util' import RpcMethodType from '@jbrowse/core/pluggableElementTypes/RpcMethodType' import ComparativeRenderer, { RenderArgs as ComparativeRenderArgs, @@ -6,7 +5,7 @@ import ComparativeRenderer, { RenderResults, ResultsSerialized, } from '@jbrowse/core/pluggableElementTypes/renderers/ComparativeServerSideRendererType' -import { RemoteAbortSignal } from '@jbrowse/core/rpc/remoteAbortSignals' +import { checkStopToken } from '@jbrowse/core/util/stopToken' interface RenderArgs extends ComparativeRenderArgs { adapterConfig: Record @@ -47,19 +46,19 @@ export default class ComparativeRender extends RpcMethodType { } async execute( - args: RenderArgsSerialized & { signal?: RemoteAbortSignal }, + args: RenderArgsSerialized & { stopToken?: string }, rpcDriver: string, ) { let deserializedArgs = args if (rpcDriver !== 'MainThreadRpcDriver') { deserializedArgs = await this.deserializeArguments(args, rpcDriver) } - const { sessionId, rendererType, signal } = deserializedArgs + const { sessionId, rendererType, stopToken } = deserializedArgs if (!sessionId) { throw new Error('must pass a unique session id') } - checkAbortSignal(signal) + checkStopToken(stopToken) const renderer = this.getRenderer(rendererType) return rpcDriver === 'MainThreadRpcDriver' diff --git a/plugins/dotplot-view/src/DotplotDisplay/stateModelFactory.tsx b/plugins/dotplot-view/src/DotplotDisplay/stateModelFactory.tsx index 90dcc1ce43..a3d825ac65 100644 --- a/plugins/dotplot-view/src/DotplotDisplay/stateModelFactory.tsx +++ b/plugins/dotplot-view/src/DotplotDisplay/stateModelFactory.tsx @@ -40,13 +40,37 @@ export function stateModelFactory(configSchema: AnyConfigurationSchemaType) { configuration: ConfigurationReference(configSchema), }) .volatile(() => ({ + /** + * #volatile + */ + stopToken: undefined as string | undefined, + /** + * #volatile + */ warnings: [] as { message: string; effect: string }[], - renderInProgress: undefined as AbortController | undefined, + /** + * #volatile + */ filled: false, + /** + * #volatile + */ data: undefined as any, + /** + * #volatile + */ reactElement: undefined as React.ReactElement | undefined, + /** + * #volatile + */ message: undefined as string | undefined, + /** + * #volatile + */ renderingComponent: undefined as any, + /** + * #volatile + */ ReactComponent2: ServerSideRenderedBlockContent as unknown as React.FC, })), @@ -103,92 +127,82 @@ export function stateModelFactory(configSchema: AnyConfigurationSchemaType) { ) }, })) - .actions(self => { - let renderInProgress: undefined | AbortController - - return { - afterAttach() { - makeAbortableReaction( - self, - () => renderBlockData(self), - blockData => renderBlockEffect(blockData), - { - name: `${self.type} ${self.id} rendering`, - delay: 500, - fireImmediately: true, - }, - this.setLoading, - this.setRendered, - this.setError, - ) - }, - /** - * #action - */ - setLoading(abortController: AbortController) { - self.filled = false - self.message = undefined - self.reactElement = undefined - self.data = undefined - self.error = undefined - self.renderingComponent = undefined - renderInProgress = abortController - }, - /** - * #action - */ - setMessage(messageText: string) { - if (renderInProgress && !renderInProgress.signal.aborted) { - renderInProgress.abort() - } - self.filled = false - self.message = messageText - self.reactElement = undefined - self.data = undefined - self.error = undefined - self.renderingComponent = undefined - renderInProgress = undefined - }, - /** - * #action - */ - setRendered(args?: { - data: any - reactElement: React.ReactElement - renderingComponent: React.Component - }) { - if (args === undefined) { - return - } - const { data, reactElement, renderingComponent } = args - self.warnings = data.warnings - self.filled = true - self.message = undefined - self.reactElement = reactElement - self.data = data - self.error = undefined - self.renderingComponent = renderingComponent - renderInProgress = undefined - }, - /** - * #action - */ - setError(error: unknown) { - console.error(error) - if (renderInProgress && !renderInProgress.signal.aborted) { - renderInProgress.abort() - } - // the rendering failed for some reason - self.filled = false - self.message = undefined - self.reactElement = undefined - self.data = undefined - self.error = error - self.renderingComponent = undefined - renderInProgress = undefined - }, - } - }) + .actions(self => ({ + afterAttach() { + makeAbortableReaction( + self, + () => renderBlockData(self), + blockData => renderBlockEffect(blockData), + { + name: `${self.type} ${self.id} rendering`, + delay: 500, + fireImmediately: true, + }, + this.setLoading, + this.setRendered, + this.setError, + ) + }, + /** + * #action + */ + setLoading(stopToken?: string) { + self.filled = false + self.message = undefined + self.reactElement = undefined + self.data = undefined + self.error = undefined + self.renderingComponent = undefined + self.stopToken = stopToken + }, + /** + * #action + */ + setMessage(messageText: string) { + self.filled = false + self.message = messageText + self.reactElement = undefined + self.data = undefined + self.error = undefined + self.renderingComponent = undefined + self.stopToken = undefined + }, + /** + * #action + */ + setRendered(args?: { + data: any + reactElement: React.ReactElement + renderingComponent: React.Component + }) { + if (args === undefined) { + return + } + const { data, reactElement, renderingComponent } = args + self.warnings = data.warnings + self.filled = true + self.message = undefined + self.reactElement = reactElement + self.data = data + self.error = undefined + self.renderingComponent = renderingComponent + self.stopToken = undefined + }, + /** + * #action + */ + setError(error: unknown) { + console.error(error) + // the rendering failed for some reason + self.filled = false + self.message = undefined + self.reactElement = undefined + self.data = undefined + self.error = error + self.renderingComponent = undefined + self.stopToken = undefined + }, + })) } export type DotplotDisplayStateModel = ReturnType diff --git a/plugins/dotplot-view/src/DotplotRenderer/ComparativeRenderRpc.ts b/plugins/dotplot-view/src/DotplotRenderer/ComparativeRenderRpc.ts index 2eb41dea87..3be4229b96 100644 --- a/plugins/dotplot-view/src/DotplotRenderer/ComparativeRenderRpc.ts +++ b/plugins/dotplot-view/src/DotplotRenderer/ComparativeRenderRpc.ts @@ -1,4 +1,3 @@ -import { checkAbortSignal } from '@jbrowse/core/util' import RpcMethodType from '@jbrowse/core/pluggableElementTypes/RpcMethodType' import ComparativeRenderer, { RenderArgs as ComparativeRenderArgs, @@ -6,7 +5,7 @@ import ComparativeRenderer, { RenderResults, ResultsSerialized, } from '@jbrowse/core/pluggableElementTypes/renderers/ComparativeServerSideRendererType' -import { RemoteAbortSignal } from '@jbrowse/core/rpc/remoteAbortSignals' +import { checkStopToken } from '@jbrowse/core/util/stopToken' interface RenderArgs extends ComparativeRenderArgs { adapterConfig: Record @@ -47,19 +46,19 @@ export default class ComparativeRender extends RpcMethodType { } async execute( - args: RenderArgsSerialized & { signal?: RemoteAbortSignal }, + args: RenderArgsSerialized & { stopToken?: string }, rpcDriver: string, ) { let deserializedArgs = args if (rpcDriver !== 'MainThreadRpcDriver') { deserializedArgs = await this.deserializeArguments(args, rpcDriver) } - const { sessionId, rendererType, signal } = deserializedArgs + const { sessionId, rendererType, stopToken } = deserializedArgs if (!sessionId) { throw new Error('must pass a unique session id') } - checkAbortSignal(signal) + checkStopToken(stopToken) const renderer = this.getRenderer(rendererType) return rpcDriver === 'MainThreadRpcDriver' diff --git a/plugins/gff3/src/Gff3Adapter/Gff3Adapter.ts b/plugins/gff3/src/Gff3Adapter/Gff3Adapter.ts index 33eb927e19..20592b07e3 100644 --- a/plugins/gff3/src/Gff3Adapter/Gff3Adapter.ts +++ b/plugins/gff3/src/Gff3Adapter/Gff3Adapter.ts @@ -129,7 +129,7 @@ export default class Gff3Adapter extends BaseFeatureDataAdapter { } catch (e) { observer.error(e) } - }, opts.signal) + }, opts.stopToken) } public freeResources(/* { region } */) {} diff --git a/plugins/gff3/src/Gff3TabixAdapter/Gff3TabixAdapter.ts b/plugins/gff3/src/Gff3TabixAdapter/Gff3TabixAdapter.ts index abc7c45b28..f76424c8c7 100644 --- a/plugins/gff3/src/Gff3TabixAdapter/Gff3TabixAdapter.ts +++ b/plugins/gff3/src/Gff3TabixAdapter/Gff3TabixAdapter.ts @@ -69,7 +69,7 @@ export default class Gff3TabixAdapter extends BaseFeatureDataAdapter { return ObservableCreate(async observer => { const metadata = await this.gff.getMetadata() await this.getFeaturesHelper(query, opts, metadata, observer, true) - }, opts.signal) + }, opts.stopToken) } private async getFeaturesHelper( diff --git a/plugins/grid-bookmark/src/GridBookmarkWidget/sessionSharing.ts b/plugins/grid-bookmark/src/GridBookmarkWidget/sessionSharing.ts index abc5b66e4e..9e1c9a88cd 100644 --- a/plugins/grid-bookmark/src/GridBookmarkWidget/sessionSharing.ts +++ b/plugins/grid-bookmark/src/GridBookmarkWidget/sessionSharing.ts @@ -70,17 +70,12 @@ export async function readSessionFromDynamo( baseUrl: string, sessionQueryParam: string, password: string, - signal?: AbortSignal, ) { const sessionId = sessionQueryParam.split('share-')[1]! const url = `${baseUrl}?sessionId=${encodeURIComponent(sessionId)}` - const response = await fetch(url, { - signal, - }) - + const response = await fetch(url) if (!response.ok) { - const err = await response.text() - throw new Error(getErrorMsg(err)) + throw new Error(getErrorMsg(await response.text())) } const json = await response.json() diff --git a/plugins/gtf/src/GtfAdapter/GtfAdapter.ts b/plugins/gtf/src/GtfAdapter/GtfAdapter.ts index 38f21c422a..832f5c8980 100644 --- a/plugins/gtf/src/GtfAdapter/GtfAdapter.ts +++ b/plugins/gtf/src/GtfAdapter/GtfAdapter.ts @@ -128,7 +128,7 @@ export default class GtfAdapter extends BaseFeatureDataAdapter { } catch (e) { observer.error(e) } - }, opts.signal) + }, opts.stopToken) } public freeResources(/* { region } */) {} } diff --git a/plugins/hic/src/HicAdapter/HicAdapter.ts b/plugins/hic/src/HicAdapter/HicAdapter.ts index 9ba3437b0e..6db650e247 100644 --- a/plugins/hic/src/HicAdapter/HicAdapter.ts +++ b/plugins/hic/src/HicAdapter/HicAdapter.ts @@ -120,7 +120,7 @@ export default class HicAdapter extends BaseFeatureDataAdapter { } }) observer.complete() - }, opts.signal) as any + }, opts.stopToken) as any } // don't do feature stats estimation, similar to bigwigadapter diff --git a/plugins/hic/src/HicRenderer/makeImageData.ts b/plugins/hic/src/HicRenderer/makeImageData.ts index 8a848ec604..cdfacdcbb3 100644 --- a/plugins/hic/src/HicRenderer/makeImageData.ts +++ b/plugins/hic/src/HicRenderer/makeImageData.ts @@ -1,6 +1,5 @@ import { RenderArgs as ServerSideRenderArgs } from '@jbrowse/core/pluggableElementTypes/renderers/ServerSideRendererType' import { Region } from '@jbrowse/core/util/types' -import { abortBreakPoint } from '@jbrowse/core/util' import { readConfObject } from '@jbrowse/core/configuration' import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter' import { getAdapter } from '@jbrowse/core/data_adapters/dataAdapterCache' @@ -14,6 +13,7 @@ import PluginManager from '@jbrowse/core/PluginManager' import interpolateViridis from './viridis' import { RenderArgsDeserializedWithFeatures } from './HicRenderer' +import { checkStopToken } from '@jbrowse/core/util/stopToken' interface HicDataAdapter extends BaseFeatureDataAdapter { getResolution: (bp: number) => Promise @@ -31,7 +31,7 @@ export async function makeImageData( features, config, bpPerPx, - signal, + stopToken, resolution, sessionId, adapterConfig, @@ -58,13 +58,13 @@ export async function makeImageData( let maxScore = 0 let minBin = 0 let maxBin = 0 - await abortBreakPoint(signal) + checkStopToken(stopToken) for (const { bin1, bin2, counts } of features) { maxScore = Math.max(counts, maxScore) minBin = Math.min(Math.min(bin1, bin2), minBin) maxBin = Math.max(Math.max(bin1, bin2), maxBin) } - await abortBreakPoint(signal) + checkStopToken(stopToken) const colorSchemes = { juicebox: ['rgba(0,0,0,0)', 'red'], fall: interpolateRgbBasis([ @@ -96,7 +96,7 @@ export async function makeImageData( ctx.translate(-width, 0) } ctx.rotate(-Math.PI / 4) - let start = Date.now() + let start = performance.now() for (const { bin1, bin2, counts } of features) { ctx.fillStyle = readConfObject(config, 'color', { count: counts, @@ -106,9 +106,9 @@ export async function makeImageData( useLogScale, }) ctx.fillRect((bin1 - offset) * w, (bin2 - offset) * w, w, w) - if (+Date.now() - start > 400) { - await abortBreakPoint(signal) - start = +Date.now() + if (performance.now() - start > 400) { + checkStopToken(stopToken) + start = performance.now() } } ctx.restore() diff --git a/plugins/legacy-jbrowse/src/NCListAdapter/NCListAdapter.ts b/plugins/legacy-jbrowse/src/NCListAdapter/NCListAdapter.ts index 50bc060971..24c8768d94 100644 --- a/plugins/legacy-jbrowse/src/NCListAdapter/NCListAdapter.ts +++ b/plugins/legacy-jbrowse/src/NCListAdapter/NCListAdapter.ts @@ -6,12 +6,13 @@ import { } from '@jbrowse/core/data_adapters/BaseAdapter' import { Feature } from '@jbrowse/core/util/simpleFeature' import { ObservableCreate } from '@jbrowse/core/util/rxjs' -import { checkAbortSignal } from '@jbrowse/core/util' +import { checkStopToken } from '@jbrowse/core/util/stopToken' import { RemoteFile } from 'generic-filehandle' -import NCListFeature from './NCListFeature' import PluginManager from '@jbrowse/core/PluginManager' import { getSubAdapterType } from '@jbrowse/core/data_adapters/dataAdapterCache' import { AnyConfigurationModel } from '@jbrowse/core/configuration' +// locals +import NCListFeature from './NCListFeature' export default class NCListAdapter extends BaseFeatureDataAdapter { private nclist: any @@ -47,14 +48,14 @@ export default class NCListAdapter extends BaseFeatureDataAdapter { * want to verify that the store has features for the given reference sequence * before fetching. * @param region - - * @param opts - [signal] optional signalling object for aborting the fetch + * @param opts - [stopToken] optional stopTokenling object for aborting the fetch * @returns Observable of Feature objects in the region */ getFeatures(region: Region, opts: BaseOptions = {}) { return ObservableCreate(async observer => { - const { signal } = opts + const { stopToken } = opts for await (const feature of this.nclist.getFeatures(region, opts)) { - checkAbortSignal(signal) + checkStopToken(stopToken) observer.next(this.wrapFeature(feature)) } observer.complete() diff --git a/plugins/linear-comparative-view/src/LinearComparativeDisplay/stateModelFactory.ts b/plugins/linear-comparative-view/src/LinearComparativeDisplay/stateModelFactory.ts index 210ed400aa..b8d6526543 100644 --- a/plugins/linear-comparative-view/src/LinearComparativeDisplay/stateModelFactory.ts +++ b/plugins/linear-comparative-view/src/LinearComparativeDisplay/stateModelFactory.ts @@ -14,6 +14,7 @@ import { import { getRpcSessionId } from '@jbrowse/core/util/tracks' import { BaseDisplay } from '@jbrowse/core/pluggableElementTypes/models' import { LinearComparativeViewModel } from '../LinearComparativeView/model' +import { stopStopToken } from '@jbrowse/core/util/stopToken' /** * #stateModel LinearComparativeDisplay @@ -37,7 +38,7 @@ function stateModelFactory(configSchema: AnyConfigurationSchemaType) { }), ) .volatile((/* self */) => ({ - renderInProgress: undefined as AbortController | undefined, + renderInProgress: undefined as string | undefined, features: undefined as Feature[] | undefined, message: undefined as string | undefined, })) @@ -66,17 +67,17 @@ function stateModelFactory(configSchema: AnyConfigurationSchemaType) { }, })) .actions(self => { - let renderInProgress: undefined | AbortController + let stopToken: undefined | string return { /** * #action * controlled by a reaction */ - setLoading(abortController: AbortController) { + setLoading(newStopToken: string) { self.message = undefined self.error = undefined - renderInProgress = abortController + stopToken = newStopToken }, /** @@ -84,12 +85,10 @@ function stateModelFactory(configSchema: AnyConfigurationSchemaType) { * controlled by a reaction */ setMessage(messageText: string) { - if (renderInProgress && !renderInProgress.signal.aborted) { - renderInProgress.abort() - } + // TODO:ABORT(??) self.message = messageText self.error = undefined - renderInProgress = undefined + stopToken = undefined }, /** @@ -123,7 +122,7 @@ function stateModelFactory(configSchema: AnyConfigurationSchemaType) { self.message = undefined self.error = undefined - renderInProgress = undefined + stopToken = undefined if ( foundNewFeatureNotInExistingMap || @@ -140,13 +139,13 @@ function stateModelFactory(configSchema: AnyConfigurationSchemaType) { */ setError(error: unknown) { console.error(error) - if (renderInProgress && !renderInProgress.signal.aborted) { - renderInProgress.abort() + if (stopToken !== undefined) { + stopStopToken(stopToken) } // the rendering failed for some reason self.message = undefined self.error = error - renderInProgress = undefined + stopToken = undefined }, } }) diff --git a/plugins/linear-genome-view/src/BaseLinearDisplay/models/autorunFeatureDensityStats.ts b/plugins/linear-genome-view/src/BaseLinearDisplay/models/autorunFeatureDensityStats.ts index 8e6bd391d7..740bd30c8a 100644 --- a/plugins/linear-genome-view/src/BaseLinearDisplay/models/autorunFeatureDensityStats.ts +++ b/plugins/linear-genome-view/src/BaseLinearDisplay/models/autorunFeatureDensityStats.ts @@ -1,4 +1,4 @@ -import { getContainingView, isAbortException } from '@jbrowse/core/util' +import { getContainingView } from '@jbrowse/core/util' import { LinearGenomeViewModel } from '../../LinearGenomeView' import { isAlive } from 'mobx-state-tree' import { BaseLinearDisplayModel } from './BaseLinearDisplayModel' @@ -38,8 +38,8 @@ export default async function autorunFeatureDensityStats( self.setFeatureDensityStats(stats) } } catch (e) { - if (!isAbortException(e) && isAlive(self)) { - console.error(e) + console.error(e) + if (isAlive(self)) { self.setError(e) } } diff --git a/plugins/linear-genome-view/src/BaseLinearDisplay/models/serverSideRenderedBlock.ts b/plugins/linear-genome-view/src/BaseLinearDisplay/models/serverSideRenderedBlock.ts index e275831f88..d8fb8e77f6 100644 --- a/plugins/linear-genome-view/src/BaseLinearDisplay/models/serverSideRenderedBlock.ts +++ b/plugins/linear-genome-view/src/BaseLinearDisplay/models/serverSideRenderedBlock.ts @@ -30,180 +30,230 @@ import { // locals import ServerSideRenderedBlockContent from '../components/ServerSideRenderedBlockContent' +import { stopStopToken } from '@jbrowse/core/util/stopToken' +export interface RenderedProps { + reactElement: React.ReactElement + features: Map + layout: any + maxHeightReached: boolean + renderProps: any +} // the MST state of a single server-side-rendered block in a display const blockState = types .model('BlockState', { + /** + * #property + */ key: types.string, + /** + * #property + */ region: Region, + /** + * #property + */ reloadFlag: 0, + /** + * #property + */ isLeftEndOfDisplayedRegion: false, + /** + * #property + */ isRightEndOfDisplayedRegion: false, }) - // NOTE: all this volatile stuff has to be filled in at once, so that it stays consistent .volatile(() => ({ - renderInProgress: undefined as AbortController | undefined, + stopToken: undefined as string | undefined, + /** + * #volatile + */ filled: false, + /** + * #volatile + */ reactElement: undefined as React.ReactElement | undefined, + /** + * #volatile + */ features: undefined as Map | undefined, + /** + * #volatile + */ layout: undefined as any, + /** + * #volatile + */ status: '', + /** + * #volatile + */ error: undefined as unknown, + /** + * #volatile + */ message: undefined as string | undefined, + /** + * #volatile + */ maxHeightReached: false, + /** + * #volatile + */ ReactComponent: ServerSideRenderedBlockContent, + /** + * #volatile + */ renderProps: undefined as any, })) - .actions(self => { - let renderInProgress: undefined | AbortController - return { - doReload() { - self.reloadFlag = self.reloadFlag + 1 - }, - afterAttach() { - const display = getContainingDisplay(self) - setTimeout(() => { - if (isAlive(self)) { - makeAbortableReaction( - self as any, - renderBlockData, - renderBlockEffect, - { - name: `${display.id}/${assembleLocString( - self.region, - )} rendering`, - delay: display.renderDelay, - fireImmediately: true, - }, - this.setLoading, - this.setRendered, - this.setError, + .actions(self => ({ + /** + * #action + */ + doReload() { + self.reloadFlag = self.reloadFlag + 1 + }, + afterAttach() { + const display = getContainingDisplay(self) + setTimeout(() => { + if (isAlive(self)) { + makeAbortableReaction( + self as any, + renderBlockData, + renderBlockEffect, + { + name: `${display.id}/${assembleLocString(self.region)} rendering`, + delay: display.renderDelay, + fireImmediately: true, + }, + this.setLoading, + this.setRendered, + this.setError, + ) + } + }, display.renderDelay) + }, + /** + * #action + */ + setStatus(message: string) { + self.status = message + }, + /** + * #action + */ + setLoading(newStopToken: string) { + if (self.stopToken !== undefined) { + stopStopToken(self.stopToken) + } + self.filled = false + self.message = undefined + self.reactElement = undefined + self.features = undefined + self.layout = undefined + self.error = undefined + self.maxHeightReached = false + self.renderProps = undefined + self.stopToken = newStopToken + }, + /** + * #action + */ + setMessage(messageText: string) { + if (self.stopToken !== undefined) { + stopStopToken(self.stopToken) + } + self.filled = false + self.message = messageText + self.reactElement = undefined + self.features = undefined + self.layout = undefined + self.error = undefined + self.maxHeightReached = false + self.renderProps = undefined + self.stopToken = undefined + }, + /** + * #action + */ + setRendered(props: RenderedProps | undefined) { + if (!props) { + return + } + const { reactElement, features, layout, maxHeightReached, renderProps } = + props + self.filled = true + self.message = undefined + self.reactElement = reactElement + self.features = features + self.layout = layout + self.error = undefined + self.maxHeightReached = maxHeightReached + self.renderProps = renderProps + self.stopToken = undefined + }, + /** + * #action + */ + setError(error: unknown) { + console.error(error) + if (self.stopToken !== undefined) { + stopStopToken(self.stopToken) + } + // the rendering failed for some reason + self.filled = false + self.message = undefined + self.reactElement = undefined + self.features = undefined + self.layout = undefined + self.maxHeightReached = false + self.error = error + self.renderProps = undefined + self.stopToken = undefined + if (isRetryException(error as Error)) { + this.reload() + } + }, + /** + * #action + */ + reload() { + self.stopToken = undefined + self.filled = false + self.reactElement = undefined + self.features = undefined + self.layout = undefined + self.error = undefined + self.message = undefined + self.maxHeightReached = false + self.ReactComponent = ServerSideRenderedBlockContent + self.renderProps = undefined + getParent(self, 2).reload() + }, + beforeDestroy() { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + ;(async () => { + try { + if (self.stopToken !== undefined) { + stopStopToken(self.stopToken) + } + const display = getContainingDisplay(self) + const { rpcManager } = getSession(self) + const { rendererType } = display + const { renderArgs } = renderBlockData(cast(self)) + // renderArgs can be undefined if an error occurred in this block + if (renderArgs) { + await rendererType.freeResourcesInClient( + rpcManager, + JSON.parse(JSON.stringify(renderArgs)), ) } - }, display.renderDelay) - }, - setStatus(message: string) { - self.status = message - }, - setLoading(abortController: AbortController) { - if ( - renderInProgress !== undefined && - !renderInProgress.signal.aborted - ) { - renderInProgress.abort() - } - self.filled = false - self.message = undefined - self.reactElement = undefined - self.features = undefined - self.layout = undefined - self.error = undefined - self.maxHeightReached = false - self.renderProps = undefined - renderInProgress = abortController - }, - setMessage(messageText: string) { - if (renderInProgress && !renderInProgress.signal.aborted) { - renderInProgress.abort() - } - self.filled = false - self.message = messageText - self.reactElement = undefined - self.features = undefined - self.layout = undefined - self.error = undefined - self.maxHeightReached = false - self.renderProps = undefined - renderInProgress = undefined - }, - setRendered( - props: - | { - reactElement: React.ReactElement - features: Map - layout: any - maxHeightReached: boolean - renderProps: any - } - | undefined, - ) { - if (!props) { - return - } - const { - reactElement, - features, - layout, - maxHeightReached, - renderProps, - } = props - self.filled = true - self.message = undefined - self.reactElement = reactElement - self.features = features - self.layout = layout - self.error = undefined - self.maxHeightReached = maxHeightReached - self.renderProps = renderProps - renderInProgress = undefined - }, - setError(error: unknown) { - console.error(error) - if (renderInProgress && !renderInProgress.signal.aborted) { - renderInProgress.abort() + } catch (e) { + console.error('Error while destroying block', e) } - // the rendering failed for some reason - self.filled = false - self.message = undefined - self.reactElement = undefined - self.features = undefined - self.layout = undefined - self.maxHeightReached = false - self.error = error - self.renderProps = undefined - renderInProgress = undefined - if (isRetryException(error as Error)) { - this.reload() - } - }, - reload() { - self.renderInProgress = undefined - self.filled = false - self.reactElement = undefined - self.features = undefined - self.layout = undefined - self.error = undefined - self.message = undefined - self.maxHeightReached = false - self.ReactComponent = ServerSideRenderedBlockContent - self.renderProps = undefined - getParent(self, 2).reload() - }, - beforeDestroy() { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - ;(async () => { - try { - if (renderInProgress && !renderInProgress.signal.aborted) { - renderInProgress.abort() - } - const display = getContainingDisplay(self) - const { rpcManager } = getSession(self) - const { rendererType } = display - const { renderArgs } = renderBlockData(cast(self)) - // renderArgs can be undefined if an error occurred in this block - if (renderArgs) { - await rendererType.freeResourcesInClient( - rpcManager, - JSON.parse(JSON.stringify(renderArgs)), - ) - } - } catch (e) { - console.error('Error while destroying block', e) - } - })() - }, - } - }) + })() + }, + })) export default blockState export type BlockStateModel = typeof blockState @@ -271,7 +321,7 @@ export function renderBlockData( async function renderBlockEffect( props: ReturnType | undefined, - signal: AbortSignal, + stopToken: string | undefined, self: BlockModel, ) { if (!props) { @@ -307,7 +357,7 @@ async function renderBlockEffect( ...renderArgs, ...renderProps, viewParams: getViewParams(self), - signal, + stopToken, }) return { reactElement, diff --git a/plugins/linear-genome-view/src/LinearGenomeView/components/GetSequenceDialog.tsx b/plugins/linear-genome-view/src/LinearGenomeView/components/GetSequenceDialog.tsx index f2995beb57..0e168381be 100644 --- a/plugins/linear-genome-view/src/LinearGenomeView/components/GetSequenceDialog.tsx +++ b/plugins/linear-genome-view/src/LinearGenomeView/components/GetSequenceDialog.tsx @@ -47,11 +47,7 @@ type LGV = LinearGenomeViewModel /** * Fetches and returns a list features for a given list of regions */ -async function fetchSequence( - model: LGV, - regions: Region[], - signal?: AbortSignal, -) { +async function fetchSequence(model: LGV, regions: Region[]) { const session = getSession(model) const { leftOffset, rightOffset } = model @@ -75,7 +71,6 @@ async function fetchSequence( adapterConfig, regions, sessionId, - signal, }) as Promise } @@ -108,7 +103,8 @@ const GetSequenceDialog = observer(function ({ if (selection.length === 0) { throw new Error('Selected region is out of bounds') } - const chunks = await fetchSequence(model, selection, controller.signal) + // TODO:ABORT + const chunks = await fetchSequence(model, selection) setSequenceChunks(chunks) } catch (e) { console.error(e) diff --git a/plugins/rdf/src/SPARQLAdapter/SPARQLAdapter.test.ts b/plugins/rdf/src/SPARQLAdapter/SPARQLAdapter.test.ts index 2f7477ac1b..af6c428e8d 100644 --- a/plugins/rdf/src/SPARQLAdapter/SPARQLAdapter.test.ts +++ b/plugins/rdf/src/SPARQLAdapter/SPARQLAdapter.test.ts @@ -44,7 +44,7 @@ test('adapter can fetch variants from volvox.vcf.gz', async () => { 'http://somesite.com/sparql?query=fakeRefNamesQuery&format=json', { headers: { accept: 'application/json,application/sparql-results+json' }, - signal: undefined, + stopToken: undefined, }, ) expect(refNames.length).toBe(17) @@ -61,7 +61,7 @@ test('adapter can fetch variants from volvox.vcf.gz', async () => { 'http://somesite.com/sparql?query=fakeSPARQLQuery-start0-end20000-chr1&format=json', { headers: { accept: 'application/json,application/sparql-results+json' }, - signal: undefined, + stopToken: undefined, }, ) @@ -78,7 +78,7 @@ test('adapter can fetch variants from volvox.vcf.gz', async () => { 'http://somesite.com/sparql?query=fakeSPARQLQuery-start0-end20000-chr80&format=json', { headers: { accept: 'application/json,application/sparql-results+json' }, - signal: undefined, + stopToken: undefined, }, ) }) diff --git a/plugins/rdf/src/SPARQLAdapter/SPARQLAdapter.ts b/plugins/rdf/src/SPARQLAdapter/SPARQLAdapter.ts index 4764991811..c12f3aa308 100644 --- a/plugins/rdf/src/SPARQLAdapter/SPARQLAdapter.ts +++ b/plugins/rdf/src/SPARQLAdapter/SPARQLAdapter.ts @@ -75,7 +75,7 @@ export default class SPARQLAdapter extends BaseFeatureDataAdapter { this.configRefNames = readConfObject(config, 'refNames') } - public async getRefNames(opts: BaseOptions = {}): Promise { + public async getRefNames(opts?: BaseOptions): Promise { if (this.refNames) { return this.refNames } @@ -96,26 +96,26 @@ export default class SPARQLAdapter extends BaseFeatureDataAdapter { ) const { refName } = query const results = await this.querySparql(filledTemplate, opts) - this.resultsToFeatures(results, refName).forEach(feature => { + const features = this.resultsToFeatures(results, refName) + for (const feature of features) { observer.next(feature) - }) + } observer.complete() - }, opts.signal) + }, opts.stopToken) } - private async querySparql(query: string, opts?: BaseOptions): Promise { + private async querySparql(query: string, _opts?: BaseOptions): Promise { let additionalQueryParams = '' if (this.additionalQueryParams.length) { additionalQueryParams = `&${this.additionalQueryParams.join('&')}` } - const signal = opts?.signal - const response = await fetch( - `${this.endpoint}?query=${query}${additionalQueryParams}`, - { - headers: { accept: 'application/json,application/sparql-results+json' }, - signal, + // TODO:ABORT + const url = `${this.endpoint}?query=${query}${additionalQueryParams}` + const response = await fetch(url, { + headers: { + accept: 'application/json,application/sparql-results+json', }, - ) + }) return response.json() } diff --git a/plugins/sequence/src/IndexedFastaAdapter/IndexedFastaAdapter.ts b/plugins/sequence/src/IndexedFastaAdapter/IndexedFastaAdapter.ts index 5785ba3aec..61cd1d9eef 100644 --- a/plugins/sequence/src/IndexedFastaAdapter/IndexedFastaAdapter.ts +++ b/plugins/sequence/src/IndexedFastaAdapter/IndexedFastaAdapter.ts @@ -22,20 +22,23 @@ export default class IndexedFastaAdapter extends BaseSequenceAdapter { private seqCache = new AbortablePromiseCache({ cache: new QuickLRU({ maxSize: 200 }), - fill: async (args: T, signal?: AbortSignal) => { + fill: async (args: T) => { const { refName, start, end, fasta } = args - return fasta.getSequence(refName, start, end, { ...args, signal }) + // TODO:ABORT + return fasta.getSequence(refName, start, end) }, }) - public async getRefNames(opts?: BaseOptions) { + public async getRefNames(_opts?: BaseOptions) { const { fasta } = await this.setup() - return fasta.getSequenceNames(opts) + // TODO:ABORT + return fasta.getSequenceNames() } - public async getRegions(opts?: BaseOptions) { + public async getRegions(_opts?: BaseOptions) { const { fasta } = await this.setup() - const seqSizes = await fasta.getSequenceSizes(opts) + // TODO:ABORT + const seqSizes = await fasta.getSequenceSizes() return Object.keys(seqSizes).map(refName => ({ refName, start: 0, @@ -72,11 +75,12 @@ export default class IndexedFastaAdapter extends BaseSequenceAdapter { return this.setupP } - public getFeatures(region: NoAssemblyRegion, opts?: BaseOptions) { + public getFeatures(region: NoAssemblyRegion, _opts?: BaseOptions) { const { refName, start, end } = region return ObservableCreate(async observer => { const { fasta } = await this.setup() - const size = await fasta.getSequenceSize(refName, opts) + // TODO:ABORT + const size = await fasta.getSequenceSize(refName) const regionEnd = Math.min(size, end) const chunks = [] const chunkSize = 128000 @@ -89,11 +93,11 @@ export default class IndexedFastaAdapter extends BaseSequenceAdapter { start: chunkStart, end: chunkStart + chunkSize, } - chunks.push( - this.seqCache.get(JSON.stringify(r), { ...r, fasta }, opts?.signal), - ) + // TODO:ABORT + chunks.push(this.seqCache.get(JSON.stringify(r), { ...r, fasta })) } const seq = (await Promise.all(chunks)) + .filter(f => !!f) .join('') .slice(start - s) .slice(0, end - start) diff --git a/plugins/svg/src/SvgFeatureRenderer/components/SvgFeatureRendering.test.tsx b/plugins/svg/src/SvgFeatureRenderer/components/SvgFeatureRendering.test.tsx index e07fd64b29..367e4e9a35 100644 --- a/plugins/svg/src/SvgFeatureRenderer/components/SvgFeatureRendering.test.tsx +++ b/plugins/svg/src/SvgFeatureRenderer/components/SvgFeatureRendering.test.tsx @@ -24,7 +24,6 @@ test('no features', () => { new PrecomputedLayout({ rectangles: {}, totalHeight: 20, - containsNoTransferables: true, maxHeightReached: false, }) } diff --git a/plugins/text-indexing/src/TextIndexRpcMethod/TextIndexRpcMethod.ts b/plugins/text-indexing/src/TextIndexRpcMethod/TextIndexRpcMethod.ts index 7577ccaa6e..8000a28513 100644 --- a/plugins/text-indexing/src/TextIndexRpcMethod/TextIndexRpcMethod.ts +++ b/plugins/text-indexing/src/TextIndexRpcMethod/TextIndexRpcMethod.ts @@ -1,6 +1,5 @@ import RpcMethodType from '@jbrowse/core/pluggableElementTypes/RpcMethodType' -import { RemoteAbortSignal } from '@jbrowse/core/rpc/remoteAbortSignals' -import { checkAbortSignal } from '@jbrowse/core/util' +import { checkStopToken } from '@jbrowse/core/util/stopToken' import { indexTracks, indexType, Track } from '@jbrowse/text-indexing' export class TextIndexRpcMethod extends RpcMethodType { @@ -9,7 +8,7 @@ export class TextIndexRpcMethod extends RpcMethodType { async execute( args: { sessionId: string - signal?: RemoteAbortSignal + stopToken?: string outLocation?: string attributes?: string[] exclude?: string[] @@ -18,7 +17,7 @@ export class TextIndexRpcMethod extends RpcMethodType { tracks: Track[] statusCallback: (message: string) => void }, - rpcDriverClassName: string, + _rpcDriverClassName: string, ) { const { tracks, @@ -27,11 +26,11 @@ export class TextIndexRpcMethod extends RpcMethodType { attributes, assemblies, indexType, - signal, + stopToken, statusCallback, - } = await this.deserializeArguments(args, rpcDriverClassName) + } = args - checkAbortSignal(signal) + checkStopToken(stopToken) await indexTracks({ outDir: outLocation, tracks, @@ -40,7 +39,7 @@ export class TextIndexRpcMethod extends RpcMethodType { assemblyNames: assemblies, indexType, statusCallback, - signal, + stopToken, }) } } diff --git a/plugins/variants/src/VcfAdapter/VcfAdapter.ts b/plugins/variants/src/VcfAdapter/VcfAdapter.ts index 83c5941a4d..8ef86ce8fe 100644 --- a/plugins/variants/src/VcfAdapter/VcfAdapter.ts +++ b/plugins/variants/src/VcfAdapter/VcfAdapter.ts @@ -132,7 +132,7 @@ export default class VcfAdapter extends BaseFeatureDataAdapter { } catch (e) { observer.error(e) } - }, opts.signal) + }, opts.stopToken) } public freeResources(): void {} diff --git a/plugins/variants/src/VcfTabixAdapter/VcfTabixAdapter.ts b/plugins/variants/src/VcfTabixAdapter/VcfTabixAdapter.ts index 76bc6794b5..1d709caf5c 100644 --- a/plugins/variants/src/VcfTabixAdapter/VcfTabixAdapter.ts +++ b/plugins/variants/src/VcfTabixAdapter/VcfTabixAdapter.ts @@ -82,7 +82,7 @@ export default class VcfTabixAdapter extends BaseFeatureDataAdapter { ...opts, }) observer.complete() - }, opts.signal) + }, opts.stopToken) } public freeResources(/* { region } */): void {} diff --git a/plugins/wiggle/src/BigWigAdapter/BigWigAdapter.ts b/plugins/wiggle/src/BigWigAdapter/BigWigAdapter.ts index 59a6b3f389..2dda8453e0 100644 --- a/plugins/wiggle/src/BigWigAdapter/BigWigAdapter.ts +++ b/plugins/wiggle/src/BigWigAdapter/BigWigAdapter.ts @@ -71,7 +71,7 @@ export default class BigWigAdapter extends BaseFeatureDataAdapter { const { refName, start, end } = region const { bpPerPx = 0, - signal, + stopToken, resolution = 1, statusCallback = () => {}, } = opts @@ -103,7 +103,7 @@ export default class BigWigAdapter extends BaseFeatureDataAdapter { }) } observer.complete() - }, signal) + }, stopToken) } // always render bigwig instead of calculating a feature density for it diff --git a/plugins/wiggle/src/MultiWiggleAdapter/MultiWiggleAdapter.ts b/plugins/wiggle/src/MultiWiggleAdapter/MultiWiggleAdapter.ts index 886a88232a..9de41cfcf1 100644 --- a/plugins/wiggle/src/MultiWiggleAdapter/MultiWiggleAdapter.ts +++ b/plugins/wiggle/src/MultiWiggleAdapter/MultiWiggleAdapter.ts @@ -103,7 +103,7 @@ export default class MultiWiggleAdapter extends BaseFeatureDataAdapter { ), ), ).subscribe(observer) - }, opts.signal) + }, opts.stopToken) } // always render bigwig instead of calculating a feature density for it diff --git a/plugins/wiggle/src/WiggleRPC/MultiWiggleGetSources.ts b/plugins/wiggle/src/WiggleRPC/MultiWiggleGetSources.ts index db18d2c129..cfd26d2556 100644 --- a/plugins/wiggle/src/WiggleRPC/MultiWiggleGetSources.ts +++ b/plugins/wiggle/src/WiggleRPC/MultiWiggleGetSources.ts @@ -3,7 +3,6 @@ import SerializableFilterChain from '@jbrowse/core/pluggableElementTypes/rendere import { getAdapter } from '@jbrowse/core/data_adapters/dataAdapterCache' import { RenderArgs } from '@jbrowse/core/rpc/coreRpcMethods' import { renameRegionsIfNeeded, Region } from '@jbrowse/core/util' -import { RemoteAbortSignal } from '@jbrowse/core/rpc/remoteAbortSignals' import { AnyConfigurationModel } from '@jbrowse/core/configuration' export class MultiWiggleGetSources extends RpcMethodType { @@ -23,7 +22,7 @@ export class MultiWiggleGetSources extends RpcMethodType { async serializeArguments( args: RenderArgs & { - signal?: AbortSignal + stopToken?: string statusCallback?: (arg: string) => void }, rpcDriverClassName: string, @@ -45,7 +44,7 @@ export class MultiWiggleGetSources extends RpcMethodType { async execute( args: { adapterConfig: AnyConfigurationModel - signal?: RemoteAbortSignal + stopToken?: string sessionId: string headers?: Record regions: Region[] diff --git a/plugins/wiggle/src/WiggleRPC/WiggleGetGlobalQuantitativeStats.ts b/plugins/wiggle/src/WiggleRPC/WiggleGetGlobalQuantitativeStats.ts index f654601e80..9f36f25bfa 100644 --- a/plugins/wiggle/src/WiggleRPC/WiggleGetGlobalQuantitativeStats.ts +++ b/plugins/wiggle/src/WiggleRPC/WiggleGetGlobalQuantitativeStats.ts @@ -1,7 +1,6 @@ import RpcMethodType from '@jbrowse/core/pluggableElementTypes/RpcMethodType' import SerializableFilterChain from '@jbrowse/core/pluggableElementTypes/renderers/util/serializableFilterChain' import { getAdapter } from '@jbrowse/core/data_adapters/dataAdapterCache' -import { RemoteAbortSignal } from '@jbrowse/core/rpc/remoteAbortSignals' import { QuantitativeStats } from '@jbrowse/core/util/stats' import { AnyConfigurationModel } from '@jbrowse/core/configuration' @@ -23,7 +22,7 @@ export class WiggleGetGlobalQuantitativeStats extends RpcMethodType { async execute( args: { adapterConfig: AnyConfigurationModel - signal?: RemoteAbortSignal + stopToken?: string headers?: Record sessionId: string }, diff --git a/plugins/wiggle/src/WiggleRPC/WiggleGetMultiRegionQuantitativeStats.ts b/plugins/wiggle/src/WiggleRPC/WiggleGetMultiRegionQuantitativeStats.ts index a1d5ea4561..d9fb14ac57 100644 --- a/plugins/wiggle/src/WiggleRPC/WiggleGetMultiRegionQuantitativeStats.ts +++ b/plugins/wiggle/src/WiggleRPC/WiggleGetMultiRegionQuantitativeStats.ts @@ -2,7 +2,6 @@ import RpcMethodType from '@jbrowse/core/pluggableElementTypes/RpcMethodType' import SerializableFilterChain from '@jbrowse/core/pluggableElementTypes/renderers/util/serializableFilterChain' import { RenderArgs } from '@jbrowse/core/rpc/coreRpcMethods' import { getAdapter } from '@jbrowse/core/data_adapters/dataAdapterCache' -import { RemoteAbortSignal } from '@jbrowse/core/rpc/remoteAbortSignals' import { Region, renameRegionsIfNeeded } from '@jbrowse/core/util' export class WiggleGetMultiRegionQuantitativeStats extends RpcMethodType { @@ -22,7 +21,6 @@ export class WiggleGetMultiRegionQuantitativeStats extends RpcMethodType { async serializeArguments( args: RenderArgs & { - signal?: AbortSignal statusCallback?: (arg: string) => void }, rpcDriverClassName: string, @@ -44,7 +42,7 @@ export class WiggleGetMultiRegionQuantitativeStats extends RpcMethodType { async execute( args: { adapterConfig: Record - signal?: RemoteAbortSignal + stopToken?: string sessionId: string headers?: Record regions: Region[] diff --git a/plugins/wiggle/src/getMultiWiggleSourcesAutorun.ts b/plugins/wiggle/src/getMultiWiggleSourcesAutorun.ts index b07f1ac19b..c0f0417dae 100644 --- a/plugins/wiggle/src/getMultiWiggleSourcesAutorun.ts +++ b/plugins/wiggle/src/getMultiWiggleSourcesAutorun.ts @@ -1,14 +1,11 @@ import { autorun } from 'mobx' import { addDisposer, isAlive } from 'mobx-state-tree' // jbrowse -import { - getContainingView, - getSession, - isAbortException, -} from '@jbrowse/core/util' +import { getContainingView, getSession } from '@jbrowse/core/util' import { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view' import { AnyConfigurationModel } from '@jbrowse/core/configuration' import { getRpcSessionId } from '@jbrowse/core/util/tracks' +import { isAbortException } from '@jbrowse/core/util/aborting' export interface Source { name: string @@ -51,8 +48,8 @@ export function getMultiWiggleSourcesAutorun(self: { self.setSources(sources) } } catch (e) { + console.error(e) if (!isAbortException(e) && isAlive(self)) { - console.error(e) getSession(self).notifyError(`${e}`, e) } } diff --git a/plugins/wiggle/src/getQuantitativeStats.ts b/plugins/wiggle/src/getQuantitativeStats.ts index d0fb751f54..93d4cf638f 100644 --- a/plugins/wiggle/src/getQuantitativeStats.ts +++ b/plugins/wiggle/src/getQuantitativeStats.ts @@ -17,7 +17,7 @@ export async function getQuantitativeStats( }, opts: { headers?: Record - signal?: AbortSignal + stopToken?: string filters: string[] currStatsBpPerPx: number }, diff --git a/plugins/wiggle/src/getQuantitativeStatsAutorun.ts b/plugins/wiggle/src/getQuantitativeStatsAutorun.ts index a98d01cd74..83f6f5cd9a 100644 --- a/plugins/wiggle/src/getQuantitativeStatsAutorun.ts +++ b/plugins/wiggle/src/getQuantitativeStatsAutorun.ts @@ -1,10 +1,11 @@ import { autorun } from 'mobx' import { addDisposer, isAlive } from 'mobx-state-tree' // jbrowse -import { isAbortException, getContainingView } from '@jbrowse/core/util' +import { getContainingView } from '@jbrowse/core/util' import { QuantitativeStats } from '@jbrowse/core/util/stats' import { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view' import { AnyConfigurationModel } from '@jbrowse/core/configuration' +import { createStopToken } from '@jbrowse/core/util/stopToken' // locals import { getQuantitativeStats } from './getQuantitativeStats' @@ -16,7 +17,7 @@ export function getQuantitativeStatsAutorun(self: { adapterConfig: AnyConfigurationModel autoscaleType: string adapterProps: () => Record - setStatsLoading: (aborter: AbortController) => void + setStatsLoading: (token: string) => void setError: (error: unknown) => void setMessage: (str: string) => void updateQuantitativeStats: (stats: QuantitativeStats, region: string) => void @@ -26,28 +27,25 @@ export function getQuantitativeStatsAutorun(self: { autorun( async () => { try { - const view = getContainingView(self) as LGV - const aborter = new AbortController() - self.setStatsLoading(aborter) + if (self.quantitativeStatsReady) { + const view = getContainingView(self) as LGV + const stopToken = createStopToken() + self.setStatsLoading(stopToken) + const statsRegion = JSON.stringify(view.dynamicBlocks) + const wiggleStats = await getQuantitativeStats(self, { + stopToken, + filters: [], + currStatsBpPerPx: view.bpPerPx, + ...self.adapterProps(), + }) - if (!self.quantitativeStatsReady) { - return - } - - const statsRegion = JSON.stringify(view.dynamicBlocks) - const wiggleStats = await getQuantitativeStats(self, { - signal: aborter.signal, - filters: [], - currStatsBpPerPx: view.bpPerPx, - ...self.adapterProps(), - }) - - if (isAlive(self)) { - self.updateQuantitativeStats(wiggleStats, statsRegion) + if (isAlive(self)) { + self.updateQuantitativeStats(wiggleStats, statsRegion) + } } } catch (e) { console.error(e) - if (!isAbortException(e) && isAlive(self)) { + if (isAlive(self)) { self.setError(e) } } diff --git a/plugins/wiggle/src/shared/SharedWiggleMixin.ts b/plugins/wiggle/src/shared/SharedWiggleMixin.ts index ffe8e32741..896e367c9a 100644 --- a/plugins/wiggle/src/shared/SharedWiggleMixin.ts +++ b/plugins/wiggle/src/shared/SharedWiggleMixin.ts @@ -108,7 +108,7 @@ export default function SharedWiggleMixin( /** * #volatile */ - statsFetchInProgress: undefined as undefined | AbortController, + statsFetchInProgress: undefined as undefined | string, })) .actions(self => ({ /** @@ -155,14 +155,8 @@ export default function SharedWiggleMixin( /** * #action */ - setStatsLoading(aborter: AbortController) { - if ( - self.statsFetchInProgress !== undefined && - !self.statsFetchInProgress.signal.aborted - ) { - self.statsFetchInProgress.abort() - } - self.statsFetchInProgress = aborter + setStatsLoading() { + /* do nothing */ }, /** diff --git a/products/jbrowse-desktop/src/indexJobsModel.ts b/products/jbrowse-desktop/src/indexJobsModel.ts index 02196dd7a5..9f0872a5f5 100644 --- a/products/jbrowse-desktop/src/indexJobsModel.ts +++ b/products/jbrowse-desktop/src/indexJobsModel.ts @@ -11,7 +11,7 @@ import { createTextSearchConf, findTrackConfigsToIndex, } from '@jbrowse/text-indexing' -import { isAbortException, isSessionModelWithWidgets } from '@jbrowse/core/util' +import { isSessionModelWithWidgets } from '@jbrowse/core/util' import path from 'path' import fs from 'fs' @@ -61,10 +61,6 @@ export default function jobsModelFactory(_pluginManager: PluginManager) { * #volatile */ jobName: '', - /** - * #volatile - */ - controller: new AbortController(), /** * #volatile */ @@ -162,12 +158,7 @@ export default function jobsModelFactory(_pluginManager: PluginManager) { setStatusMessage(arg: string) { self.statusMessage = arg }, - /** - * #action - */ - abortJob() { - self.controller.abort() - }, + /** * #action */ @@ -216,7 +207,6 @@ export default function jobsModelFactory(_pluginManager: PluginManager) { this.setStatusMessage('') this.setJobName('') self.progressPct = 0 - self.controller = new AbortController() }, /** * #action @@ -238,8 +228,6 @@ export default function jobsModelFactory(_pluginManager: PluginManager) { try { this.setRunning(true) this.setJobName(entry.name) - const { signal } = self.controller - const userData = await ipcRenderer.invoke('userData') const outLocation = path.join( userData, @@ -258,7 +246,6 @@ export default function jobsModelFactory(_pluginManager: PluginManager) { statusCallback: (message: string) => { this.setProgressPct(message) }, - signal, timeout: 1000 * ONE_HOUR, }) if (indexType === 'perTrack') { @@ -314,20 +301,17 @@ export default function jobsModelFactory(_pluginManager: PluginManager) { } } catch (e) { console.error(e) - if (isAbortException(e)) { - self.session?.notify('Cancelled job', 'info') - } else { - self.session?.notify( - `An error occurred while indexing: ${e}`, - 'error', - { - name: 'Retry', - onClick: () => { - this.queueJob(entry) - }, + + self.session?.notify( + `An error occurred while indexing: ${e}`, + 'error', + { + name: 'Retry', + onClick: () => { + this.queueJob(entry) }, - ) - } + }, + ) // remove job from queue but since it was not successful // do not add to finished list this.dequeueJob() diff --git a/products/jbrowse-desktop/src/sessionModel/TrackMenu.ts b/products/jbrowse-desktop/src/sessionModel/TrackMenu.ts index 4ac68d5628..ea3712814e 100644 --- a/products/jbrowse-desktop/src/sessionModel/TrackMenu.ts +++ b/products/jbrowse-desktop/src/sessionModel/TrackMenu.ts @@ -103,9 +103,6 @@ export function DesktopSessionTrackMenuMixin(_pluginManager: PluginManager) { name: indexName, }, name: indexName, - cancelCallback: () => { - jobsManager.abortJob() - }, }) }, icon: Indexing, diff --git a/test_data/hs1/config.json b/test_data/hs1/config.json index 2ee6df7c37..d3ea117d3b 100644 --- a/test_data/hs1/config.json +++ b/test_data/hs1/config.json @@ -57,6 +57,19 @@ } }, "assemblyNames": ["hs1"] + }, + { + "type": "FeatureTrack", + "trackId": "chm13v2.0_rmsk", + "name": "chm13v2.0_rmsk", + "adapter": { + "type": "BigBedAdapter", + "bigBedLocation": { + "uri": "https://hgdownload.soe.ucsc.edu/gbdb/hs1/t2tRepeatMasker/chm13v2.0_rmsk.bb", + "locationType": "UriLocation" + } + }, + "assemblyNames": ["hs1"] } ] } diff --git a/website/blog/2021-03-29-v1.1.0-release.md b/website/blog/2021-03-29-v1.1.0-release.md index a5417ee08a..67bc4585b0 100644 --- a/website/blog/2021-03-29-v1.1.0-release.md +++ b/website/blog/2021-03-29-v1.1.0-release.md @@ -94,7 +94,7 @@ details. rendering outside it's allowed bounds ([@cmdcolin](https://github.com/cmdcolin)) - [#1783](https://github.com/GMOD/jbrowse-components/pull/1783) Add hic - aborting and fix remoteAbort signal propagation + aborting and fix remoteAbort stopToken propagation ([@cmdcolin](https://github.com/cmdcolin)) - [#1723](https://github.com/GMOD/jbrowse-components/pull/1723) A few bugfixes ([@garrettjstevens](https://github.com/garrettjstevens)) diff --git a/website/blog/2022-12-16-yearinreview.md b/website/blog/2022-12-16-yearinreview.md index 936ee6cab8..5b8f063c5c 100644 --- a/website/blog/2022-12-16-yearinreview.md +++ b/website/blog/2022-12-16-yearinreview.md @@ -132,7 +132,7 @@ Screenshot of the Reactome plugin, showing the pathway viewer ## Multi-wiggle tracks We added the ability to have "multi-wiggle" tracks in v2.1.0, which let's you -see multiple quantitative signals on the same Y-scalebar easily. +see multiple quantitative stopTokens on the same Y-scalebar easily. ![](https://user-images.githubusercontent.com/6511937/181639797-69294456-cbe6-403a-9131-98af27c849f3.png) diff --git a/website/blog/2024-07-31-v2.13.1-release.md b/website/blog/2024-07-31-v2.13.1-release.md index 2553fe9d58..b15c8f775d 100644 --- a/website/blog/2024-07-31-v2.13.1-release.md +++ b/website/blog/2024-07-31-v2.13.1-release.md @@ -40,7 +40,7 @@ yarn run v1.22.22 $ lerna-changelog --silent --silent --next-version 2.13.1 track selector in linear synteny view causing crash in v2.13.0 ([@cmdcolin](https://github.com/cmdcolin)) - [#4495](https://github.com/GMOD/jbrowse-components/pull/4495) Fix log scale - for some types of signal tracks ([@cmdcolin](https://github.com/cmdcolin)) + for some types of stopToken tracks ([@cmdcolin](https://github.com/cmdcolin)) #### Committers: 1 diff --git a/website/docs/config_guides/quantitative_track.md b/website/docs/config_guides/quantitative_track.md index 8fa6393f33..79a3189a2d 100644 --- a/website/docs/config_guides/quantitative_track.md +++ b/website/docs/config_guides/quantitative_track.md @@ -27,7 +27,7 @@ Example QuantitativeTrack config: - `scaleType` - options: linear, log, to display the coverage data. default: linear -- `adapter` - an adapter that returns numeric signal data, e.g. +- `adapter` - an adapter that returns numeric stopToken data, e.g. feature.get('score') #### Autoscale options for QuantitativeTrack diff --git a/website/docs/developer_guides/creating_adapter.md b/website/docs/developer_guides/creating_adapter.md index e99ad1283a..eae43de45b 100644 --- a/website/docs/developer_guides/creating_adapter.md +++ b/website/docs/developer_guides/creating_adapter.md @@ -71,7 +71,7 @@ class MyAdapter extends BaseFeatureDataAdapter { // originalRefName:string the name of the refName from the fasta file, e.g. 1 instead of chr1 // } // opts: { - // signal?: AbortSignal + // stopToken?: string // ...rest: all the renderProps() object from the display type // } } @@ -199,7 +199,7 @@ The options parameter to getFeatures can contain any number of things: ```typescript interface Options { bpPerPx: number - signal: AbortSignal + stopToken?: string statusCallback: Function headers: Record } @@ -207,8 +207,8 @@ interface Options { - `bpPerPx` - number: resolution of the genome browser when the features were fetched -- `signal` - can be used to abort a fetch request when it is no longer needed, - from AbortController +- `stopToken` - can be used to abort a fetch request when it is no longer + needed, from AbortController - `statusCallback` - not implemented yet but in the future may allow you to report the status of your loading operations - `headers` - set of HTTP headers as a JSON object diff --git a/website/docs/user_guides/alignments_track.md b/website/docs/user_guides/alignments_track.md index c3d7cee96e..09ba9aba22 100644 --- a/website/docs/user_guides/alignments_track.md +++ b/website/docs/user_guides/alignments_track.md @@ -34,8 +34,8 @@ either be removed from the alignment (hard clipping) or can be included, and not shown by default (soft clipping). JBrowse 2 also contains an option to "show the soft clipping" that has occurred. -This can be valuable to show the signal around a region that contains structural -variation or difficult mappability. +This can be valuable to show the stopToken around a region that contains +structural variation or difficult mappability.
diff --git a/website/docs/user_guides/multiquantitative_track.md b/website/docs/user_guides/multiquantitative_track.md index 915d33667e..9e719c7b7d 100644 --- a/website/docs/user_guides/multiquantitative_track.md +++ b/website/docs/user_guides/multiquantitative_track.md @@ -6,7 +6,7 @@ title: Multi-quantitative tracks import Figure from '../figure' In 2.1.0, we created the ability to have "Multi-quantitative tracks" which is a -single track composed of multiple quantitative signals, which have their +single track composed of multiple quantitative stopTokens, which have their Y-scalebar synchronized. There are 5 rendering modes for the multi-quantitative tracks. diff --git a/website/docs/user_guides/quantitative_track.md b/website/docs/user_guides/quantitative_track.md index bdaae5a54e..1d60e2cdb9 100644 --- a/website/docs/user_guides/quantitative_track.md +++ b/website/docs/user_guides/quantitative_track.md @@ -5,8 +5,9 @@ title: Quantitative tracks import Figure from '../figure' -Visualizing genome signals, whether it is read depth-of-coverage or other -signal, can often be done by using BigWig or other quantitative feature files. +Visualizing genome stopTokens, whether it is read depth-of-coverage or other +stopToken, can often be done by using BigWig or other quantitative feature +files.