Skip to content

Commit

Permalink
Allow using web worker RPC on embedded LGV (#3276)
Browse files Browse the repository at this point in the history
* Add rpc web worker functionality for embedded

* Wire up

* Mock for test

* Remove inaccurate comments
  • Loading branch information
cmdcolin authored Oct 18, 2022
1 parent c234d2e commit 8885519
Show file tree
Hide file tree
Showing 22 changed files with 237 additions and 57 deletions.
5 changes: 4 additions & 1 deletion packages/core/rpc/configSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ export default ConfigurationSchema(
WebWorkerRpcDriverConfigSchema,
),
),
{ MainThreadRpcDriver: { type: 'MainThreadRpcDriver' } },
{
MainThreadRpcDriver: { type: 'MainThreadRpcDriver' },
WebWorkerRpcDriver: { type: 'WebWorkerRpcDriver' },
},
),
},
{
Expand Down
2 changes: 0 additions & 2 deletions packages/text-indexing/tsconfig.build.es5.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
{
// This config is for emitting declarations (.d.ts) only
// Actual .ts source files are transpiled via babel. This file is based on examples from the MUI github repo
"extends": "./tsconfig",
"compilerOptions": {
"declaration": true,
Expand Down
2 changes: 0 additions & 2 deletions packages/text-indexing/tsconfig.build.esm.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
{
// This config is for emitting declarations (.d.ts) only
// Actual .ts source files are transpiled via babel. This file is based on examples from the MUI github repo
"extends": "./tsconfig",
"compilerOptions": {
"declaration": true,
Expand Down
4 changes: 0 additions & 4 deletions products/jbrowse-desktop/src/StartScreen/util.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,10 +151,6 @@ export async function createPluginManager(

const config = rootModel.jbrowse.configuration
const { rpc } = config

rpc.addDriverConfig('WebWorkerRpcDriver', {
type: 'WebWorkerRpcDriver',
})
rpc.defaultDriver.set('WebWorkerRpcDriver')

pm.setRootModel(rootModel)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
{
// This config is for emitting declarations (.d.ts) only
// Actual .ts source files are transpiled via babel. This file is based on examples from the MUI github repo
"extends": "./tsconfig",
"compilerOptions": {
"noEmit": false,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
{
// This config is for emitting declarations (.d.ts) only
// Actual .ts source files are transpiled via babel. This file is based on examples from the MUI github repo
"extends": "./tsconfig",
"compilerOptions": {
"noEmit": false,
Expand Down
6 changes: 4 additions & 2 deletions products/jbrowse-react-linear-genome-view/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
"directory": "products/jbrowse-react-linear-genome-view"
},
"author": "JBrowse Team",
"main": "dist/index.js",
"module": "esm/index.js",
"main": "dist/indexES5.js",
"module": "esm/indexESM.js",
"files": [
"dist",
"src",
Expand Down Expand Up @@ -59,12 +59,14 @@
"@jbrowse/plugin-wiggle": "^2.1.5",
"@mui/icons-material": "^5.0.0",
"@mui/material": "^5.0.0",
"librpc-web-mod": "^1.1.5",
"mobx": "^6.6.0",
"mobx-react": "^7.5.0",
"mobx-state-tree": "^5.0.0",
"prop-types": "^15.0.0",
"react-use-measure": "^2.1.1",
"rxjs": "^6.0.0",
"serialize-error": "^8.0.0",
"tss-react": "^3.7.0"
},
"peerDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import React, { Suspense } from 'react'
import { render } from '@testing-library/react'
import { createViewState } from '..'
import JBrowseLinearGenomeView from './JBrowseLinearGenomeView'
jest.mock(
'@jbrowse/react-linear-genome-view/src/makeWorkerInstance',
() => () => {},
)

window.requestIdleCallback = (
cb: (deadline: { didTimeout: boolean; timeRemaining: () => number }) => void,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import corePlugins from '../corePlugins'
import createConfigModel from './createConfigModel'
import createSessionModel from './createSessionModel'

export default function createModel(runtimePlugins: PluginConstructor[]) {
export default function createModel(
runtimePlugins: PluginConstructor[],
makeWorkerInstance: () => Worker = () => {
throw new Error('no makeWorkerInstance supplied')
},
) {
const pluginManager = new PluginManager(
[...corePlugins, ...runtimePlugins].map(P => new P()),
)
Expand All @@ -25,9 +30,17 @@ export default function createModel(runtimePlugins: PluginConstructor[]) {
assemblyManager: types.optional(AssemblyManager, {}),
disableAddTracks: types.optional(types.boolean, false),
})
.volatile(() => ({
.volatile(self => ({
error: undefined as Error | undefined,
rpcManager: new RpcManager(pluginManager, self.config.configuration.rpc, {
WebWorkerRpcDriver: {
makeWorkerInstance,
},
MainThreadRpcDriver: {},
}),
textSearchManager: new TextSearchManager(pluginManager),
}))

.actions(self => ({
setSession(sessionSnapshot: SnapshotIn<typeof Session>) {
self.session = cast(sessionSnapshot)
Expand All @@ -43,17 +56,12 @@ export default function createModel(runtimePlugins: PluginConstructor[]) {
self.error = errorMessage
},
}))

.views(self => ({
get jbrowse() {
return self.config
},
}))
.volatile(self => ({
rpcManager: new RpcManager(pluginManager, self.config.configuration.rpc, {
MainThreadRpcDriver: {},
}),
textSearchManager: new TextSearchManager(pluginManager),
}))
return { model: rootModel, pluginManager }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface ViewStateOptions {
defaultSession?: SessionSnapshot
disableAddTracks?: boolean
onChange?: (patch: IJsonPatch, reversePatch: IJsonPatch) => void
makeWorkerInstance?: () => Worker
}

export default function createViewState(opts: ViewStateOptions) {
Expand All @@ -41,8 +42,12 @@ export default function createViewState(opts: ViewStateOptions) {
location,
onChange,
disableAddTracks = false,
makeWorkerInstance,
} = opts
const { model, pluginManager } = createModel(plugins || [])
const { model, pluginManager } = createModel(
plugins || [],
makeWorkerInstance,
)
let { defaultSession } = opts
if (!defaultSession) {
defaultSession = {
Expand Down
6 changes: 6 additions & 0 deletions products/jbrowse-react-linear-genome-view/src/indexES5.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { createJBrowseTheme, ThemeProvider } from './deprecations'
export { default as JBrowseLinearGenomeView } from './JBrowseLinearGenomeView'
export { default as createModel } from './createModel'
export { default as createViewState } from './createViewState'
export { default as loadPlugins } from './loadPlugins'
export type { ViewModel } from './createModel/createModel'
7 changes: 7 additions & 0 deletions products/jbrowse-react-linear-genome-view/src/indexESM.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export { createJBrowseTheme, ThemeProvider } from './deprecations'
export { default as JBrowseLinearGenomeView } from './JBrowseLinearGenomeView'
export { default as createModel } from './createModel'
export { default as createViewState } from './createViewState'
export { default as loadPlugins } from './loadPlugins'
export { default as makeWorkerInstance } from './makeWorkerInstance'
export type { ViewModel } from './createModel/createModel'
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// this is in a separate module here so it can be mocked out by jest. the
// import.meta.url is not well recognized by jest and we use MainThreadRpc in
// tests anyways right now
//
// note: this uses webpack 5 native worker modules
//
// see https://github.com/cmdcolin/cra-webpack5-web-worker-example for simple example
// and docs https://webpack.js.org/guides/web-workers/
//
// also note: the craco config for webpack publicPath: 'auto' is needed for
// these workers
export default function makeWorkerInstance() {
return new Worker(new URL('./rpcWorker', import.meta.url))
}
106 changes: 106 additions & 0 deletions products/jbrowse-react-linear-genome-view/src/rpcWorker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/* eslint-disable no-restricted-globals */
import './workerPolyfill'

// @ts-ignore
import RpcServer from 'librpc-web-mod'
import { enableStaticRendering } from 'mobx-react'

import PluginManager from '@jbrowse/core/PluginManager'
import { remoteAbortRpcHandler } from '@jbrowse/core/rpc/remoteAbortSignals'
import PluginLoader, { PluginDefinition } from '@jbrowse/core/PluginLoader'
import { serializeError } from 'serialize-error'

// locals
import corePlugins from './corePlugins'

// static rendering is used for "SSR" style rendering which is done on the
// worker
enableStaticRendering(true)

interface WorkerConfiguration {
plugins: PluginDefinition[]
windowHref: string
}

// waits for a message from the main thread containing our configuration, which
// must be sent on boot
function receiveConfiguration() {
const configurationP: Promise<WorkerConfiguration> = new Promise(resolve => {
function listener(event: MessageEvent) {
if (event.data.message === 'config') {
resolve(event.data.config as WorkerConfiguration)
removeEventListener('message', listener)
}
}
self.addEventListener('message', listener)
})
postMessage({ message: 'readyForConfig' })
return configurationP
}

async function getPluginManager() {
// Load runtime plugins
const config = await receiveConfiguration()
const pluginLoader = new PluginLoader(config.plugins, {
fetchESM: url => import(/* webpackIgnore:true */ url),
})
pluginLoader.installGlobalReExports(self)
const runtimePlugins = await pluginLoader.load(config.windowHref)
const plugins = [...corePlugins.map(p => ({ plugin: p })), ...runtimePlugins]
const pluginManager = new PluginManager(plugins.map(P => new P.plugin()))
pluginManager.createPluggableElements()
pluginManager.configure()

return pluginManager
}

interface WrappedFuncArgs {
rpcDriverClassName: string
channel: string
[key: string]: unknown
}

type RpcFunc = (args: unknown, rpcDriverClassName: string) => unknown

function wrapForRpc(func: RpcFunc) {
return (args: WrappedFuncArgs) => {
const { channel, rpcDriverClassName } = args
return func(
{
...args,
statusCallback: (message: string) => {
// @ts-ignore
self.rpcServer.emit(channel, message)
},
},
rpcDriverClassName,
)
}
}

getPluginManager()
.then(pluginManager => {
const rpcConfig = Object.fromEntries(
pluginManager
.getRpcElements()
.map(entry => [entry.name, wrapForRpc(entry.execute.bind(entry))]),
)

// @ts-ignore
self.rpcServer = new RpcServer.Server({
...rpcConfig,
...remoteAbortRpcHandler(),
ping: () => {
// the ping method is required by the worker driver for checking the
// health of the worker
},
})
postMessage({ message: 'ready' })
})
.catch(error => {
postMessage({ message: 'error', error: serializeError(error) })
})

export default () => {
/* do nothing */
}
33 changes: 33 additions & 0 deletions products/jbrowse-react-linear-genome-view/src/workerPolyfill.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* eslint-disable no-restricted-globals */

// this is a little polyfill for running in workers that
// contains just enough stubbing to make webpack style-loader
// think that it is actually inserting styles into the DOM

self.window = {
addEventListener() {},
fetch: self.fetch.bind(self),
location: self.location,
Date: self.Date,
requestIdleCallback: cb => cb(),
cancelIdleCallback: () => {},
requestAnimationFrame: cb => cb(),
cancelAnimationFrame: () => {},
navigator: {},
}
self.document = {
createTextNode() {},
querySelector() {
return { appendChild() {} }
},
documentElement: {},
querySelectorAll: () => [],
createElement() {
return {
style: {},
setAttribute() {},
removeAttribute() {},
appendChild() {},
}
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import Plugin from '@jbrowse/core/Plugin'
// locals
import { createViewState, loadPlugins, JBrowseLinearGenomeView } from '../src'

import makeWorkerInstance from '../src/makeWorkerInstance'

// configs
import volvoxConfig from '../public/test_data/volvox/config.json'
import volvoxSession from '../public/volvox-session.json'
Expand Down Expand Up @@ -45,9 +47,7 @@ const excludeIds = ['gtf_plain_text_test', 'lollipop_track', 'arc_track']

const assembly = volvoxConfig.assemblies[0]
const tracks = volvoxConfig.tracks.filter(
track =>
supportedTrackTypes.includes(track.type) &&
!excludeIds.includes(track.trackId),
t => supportedTrackTypes.includes(t.type) && !excludeIds.includes(t.trackId),
)
const defaultSession = {
name: 'Storybook',
Expand All @@ -57,6 +57,23 @@ const longReadsSession = {
...defaultSession,
view: volvoxSession.session.views[0],
}
export const WithWebWorker = () => {
const state = createViewState({
assembly,
tracks,
location: 'ctgA:1105..1221',
configuration: {
rpc: {
defaultDriver: 'WebWorkerRpcDriver',
},
},
makeWorkerInstance,
})
state.session.view.showTrack('Deep sequencing')

return <JBrowseLinearGenomeView viewState={state} />
}

export const OneLinearGenomeView = () => {
const state = createViewState({
assembly,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
{
// This config is for emitting declarations (.d.ts) only
// Actual .ts source files are transpiled via babel. This file is based on examples from the MUI github repo
"extends": "./tsconfig",
"compilerOptions": {
"noEmit": false,
Expand All @@ -11,7 +9,12 @@
"composite": true
},
"include": ["./src/**/*.ts*", "./src/**/*.js*"],
"exclude": ["src/**/*.test.ts*", "src/**/*.test.js*"],
"exclude": [
"src/**/*.test.ts*",
"src/**/*.test.js*",
"src/makeWorkerInstance.ts",
"src/indexESM.ts"
],
"references": [
{ "path": "../../packages/core/tsconfig.build.json" },
{ "path": "../../plugins/alignments/tsconfig.build.es5.json" },
Expand Down
Loading

0 comments on commit 8885519

Please sign in to comment.