UNPKG

@jbrowse/core

Version:

JBrowse 2 core libraries used by plugins

415 lines (414 loc) 15.1 kB
import { isModelType, isType, types } from '@jbrowse/mobx-state-tree'; import CorePlugin from "./CorePlugin.js"; import PhasedScheduler from "./PhasedScheduler.js"; import ReExports from "./ReExports/index.js"; import { ConfigurationSchema, isBareConfigurationSchemaType, } from "./configuration/index.js"; import AdapterType from "./pluggableElementTypes/AdapterType.js"; import AddTrackWorkflowType from "./pluggableElementTypes/AddTrackWorkflowType.js"; import ConnectionType from "./pluggableElementTypes/ConnectionType.js"; import DisplayType from "./pluggableElementTypes/DisplayType.js"; import GlyphType from "./pluggableElementTypes/GlyphType.js"; import InternetAccountType from "./pluggableElementTypes/InternetAccountType.js"; import RpcMethodType from "./pluggableElementTypes/RpcMethodType.js"; import TextSearchAdapterType from "./pluggableElementTypes/TextSearchAdapterType.js"; import TrackType from "./pluggableElementTypes/TrackType.js"; import ViewType from "./pluggableElementTypes/ViewType.js"; import WidgetType from "./pluggableElementTypes/WidgetType.js"; import RendererType from "./pluggableElementTypes/renderers/RendererType.js"; import createJexlInstance from "./util/jexl.js"; class TypeRecord { typeName; baseClass; registeredTypes = {}; constructor(typeName, baseClass) { this.typeName = typeName; this.baseClass = baseClass; } add(name, t) { this.registeredTypes[name] = t; } has(name) { return name in this.registeredTypes; } get(name) { if (!this.has(name)) { throw new Error(`${this.typeName} '${name}' not found, perhaps its plugin is not loaded or its plugin has not added it.`); } return this.registeredTypes[name]; } all() { return Object.values(this.registeredTypes); } } export default class PluginManager { plugins = []; jexl = createJexlInstance(); pluginMetadata = {}; runtimePluginDefinitions = []; elementCreationSchedule = new PhasedScheduler('glyph', 'renderer', 'adapter', 'text search adapter', 'display', 'track', 'connection', 'view', 'widget', 'rpc method', 'internet account', 'add track workflow'); glyphTypes = new TypeRecord('GlyphType', GlyphType); rendererTypes = new TypeRecord('RendererType', RendererType); adapterTypes = new TypeRecord('AdapterType', AdapterType); textSearchAdapterTypes = new TypeRecord('TextSearchAdapterType', TextSearchAdapterType); trackTypes = new TypeRecord('TrackType', TrackType); displayTypes = new TypeRecord('DisplayType', DisplayType); connectionTypes = new TypeRecord('ConnectionType', ConnectionType); viewTypes = new TypeRecord('ViewType', ViewType); widgetTypes = new TypeRecord('WidgetType', WidgetType); rpcMethods = new TypeRecord('RpcMethodType', RpcMethodType); addTrackWidgets = new TypeRecord('AddTrackWorkflow', AddTrackWorkflowType); internetAccountTypes = new TypeRecord('InternetAccountType', InternetAccountType); configured = false; rootModel; extensionPoints = new Map(); constructor(initialPlugins = []) { this.addPlugin({ plugin: new CorePlugin(), metadata: { isCore: true, }, }); for (const plugin of initialPlugins) { this.addPlugin(plugin); } } pluginConfigurationNamespacedSchemas() { const configurationSchemas = {}; for (const plugin of this.plugins) { if (plugin.configurationSchema) { configurationSchemas[plugin.name] = plugin.configurationSchema; } } return configurationSchemas; } pluginConfigurationUnnamespacedSchemas() { let configurationSchemas = {}; for (const plugin of this.plugins) { if (plugin.configurationSchemaUnnamespaced) { configurationSchemas = { ...configurationSchemas, ...plugin.configurationSchemaUnnamespaced, }; } } return configurationSchemas; } pluginConfigurationRootSchemas() { let configurationSchemas = {}; for (const plugin of this.plugins) { if (plugin.rootConfigurationSchema) { configurationSchemas = { ...configurationSchemas, ...plugin.rootConfigurationSchema(this), }; } } return configurationSchemas; } addPlugin(load) { if (this.configured) { throw new Error('JBrowse already configured, cannot add plugins'); } const [plugin, metadata = {}] = 'install' in load && 'configure' in load ? [load, {}] : [load.plugin, load.metadata]; if (this.plugins.includes(plugin)) { throw new Error('plugin already installed'); } this.pluginMetadata[plugin.name] = metadata; if ('definition' in load) { this.runtimePluginDefinitions.push(load.definition); } plugin.install(this); this.plugins.push(plugin); return this; } getPlugin(name) { return this.plugins.find(p => p.name === name); } hasPlugin(name) { return this.getPlugin(name) !== undefined; } createPluggableElements() { if (this.elementCreationSchedule) { this.elementCreationSchedule.run(); this.elementCreationSchedule = undefined; } return this; } setRootModel(rootModel) { this.rootModel = rootModel; return this; } configure() { if (this.configured) { throw new Error('already configured'); } for (const plugin of this.plugins) { plugin.configure(this); } this.configured = true; return this; } getElementTypeRecord(groupName) { switch (groupName) { case 'adapter': return this.adapterTypes; case 'text search adapter': return this.textSearchAdapterTypes; case 'connection': return this.connectionTypes; case 'widget': return this.widgetTypes; case 'renderer': return this.rendererTypes; case 'display': return this.displayTypes; case 'track': return this.trackTypes; case 'view': return this.viewTypes; case 'rpc method': return this.rpcMethods; case 'internet account': return this.internetAccountTypes; case 'add track workflow': return this.addTrackWidgets; case 'glyph': return this.glyphTypes; default: throw new Error(`invalid element type '${groupName}'`); } } addElementType(groupName, creationCallback) { if (typeof creationCallback !== 'function') { throw new Error('must provide a callback function that returns the new type object'); } const typeRecord = this.getElementTypeRecord(groupName); this.elementCreationSchedule?.add(groupName, () => { const newElement = creationCallback(this); if (!newElement.name) { throw new Error(`cannot add a ${groupName} with no name`); } if (typeRecord.has(newElement.name)) { console.warn(`${groupName} ${newElement.name} already registered, cannot register it again`); } else { typeRecord.add(newElement.name, this.evaluateExtensionPoint('Core-extendPluggableElement', newElement)); } }); return this; } getElementType(groupName, typeName) { return this.getElementTypeRecord(groupName).get(typeName); } getElementTypesInGroup(groupName) { return this.getElementTypeRecord(groupName).all(); } getViewElements() { return this.getElementTypesInGroup('view'); } getTrackElements() { return this.getElementTypesInGroup('track'); } getConnectionElements() { return this.getElementTypesInGroup('connection'); } getAddTrackWorkflowElements() { return this.getElementTypesInGroup('add track workflow'); } getRpcElements() { return this.getElementTypesInGroup('rpc method'); } getDisplayElements() { return this.getElementTypesInGroup('display'); } getAdapterElements() { return this.getElementTypesInGroup('adapter'); } pluggableMstType(groupName, fieldName, fallback = types.maybe(types.null)) { const pluggableTypes = this.getElementTypeRecord(groupName) .all() .map(t => t[fieldName]) .filter(t => isType(t) && isModelType(t)); if (pluggableTypes.length === 0 && typeof jest === 'undefined') { console.warn(`No pluggable types found matching ('${groupName}','${fieldName}')`); return fallback; } return types.union(...pluggableTypes); } pluggableConfigSchemaType(typeGroup, fieldName = 'configSchema') { const pluggableTypes = this.getElementTypeRecord(typeGroup) .all() .map(t => t[fieldName]) .filter(t => isBareConfigurationSchemaType(t)); if (pluggableTypes.length === 0) { pluggableTypes.push(ConfigurationSchema('Null', {})); } return types.union(...pluggableTypes); } jbrequireCache = new Map(); lib = ReExports; load = (lib) => { if (!this.jbrequireCache.has(lib)) { this.jbrequireCache.set(lib, lib(this)); } return this.jbrequireCache.get(lib); }; jbrequire = (lib) => { if (typeof lib === 'string') { const pack = this.lib[lib]; if (!pack) { throw new TypeError(`No jbrequire re-export defined for package '${lib}'. If this package must be shared between plugins, add it to ReExports.js. If it does not need to be shared, just import it normally.`); } return pack; } else if (typeof lib === 'function') { return this.load(lib); } else if (lib.default) { console.warn('initiated jbrequire on a {default:Function}'); return this.jbrequire(lib.default); } throw new TypeError('lib passed to jbrequire must be either a string or a function'); }; getRendererType(typeName) { return this.rendererTypes.get(typeName); } getRendererTypes() { return this.rendererTypes.all(); } getAdapterType(typeName) { return this.adapterTypes.get(typeName); } getTextSearchAdapterType(typeName) { return this.textSearchAdapterTypes.get(typeName); } getTrackType(typeName) { return this.trackTypes.get(typeName); } getDisplayType(typeName) { return this.displayTypes.get(typeName); } getViewType(typeName) { return this.viewTypes.get(typeName); } getAddTrackWorkflow(typeName) { return this.addTrackWidgets.get(typeName); } getWidgetType(typeName) { return this.widgetTypes.get(typeName); } getConnectionType(typeName) { return this.connectionTypes.get(typeName); } getRpcMethodType(methodName) { return this.rpcMethods.get(methodName); } getInternetAccountType(name) { return this.internetAccountTypes.get(name); } addRendererType(cb) { return this.addElementType('renderer', cb); } addAdapterType(cb) { return this.addElementType('adapter', cb); } addTextSearchAdapterType(cb) { return this.addElementType('text search adapter', cb); } addTrackType(cb) { const callback = () => { const track = cb(this); const displays = this.getElementTypesInGroup('display'); for (const display of displays) { if (display.trackType === track.name && !track.displayTypes.includes(display)) { track.addDisplayType(display); } } return track; }; return this.addElementType('track', callback); } addDisplayType(cb) { return this.addElementType('display', cb); } addViewType(cb) { const callback = () => { const newView = cb(this); const displays = this.getElementTypesInGroup('display'); for (const display of displays) { if ((display.viewType === newView.name || display.viewType === newView.extendedName) && !newView.displayTypes.includes(display)) { newView.addDisplayType(display); } } return newView; }; return this.addElementType('view', callback); } addWidgetType(cb) { return this.addElementType('widget', cb); } addConnectionType(cb) { return this.addElementType('connection', cb); } addRpcMethod(cb) { return this.addElementType('rpc method', cb); } addInternetAccountType(cb) { return this.addElementType('internet account', cb); } addAddTrackWorkflowType(cb) { return this.addElementType('add track workflow', cb); } addGlyphType(cb) { return this.addElementType('glyph', cb); } getGlyphType(typeName) { return this.glyphTypes.get(typeName); } getGlyphTypes() { return this.glyphTypes.all(); } addToExtensionPoint(extensionPointName, callback) { let callbacks = this.extensionPoints.get(extensionPointName); if (!callbacks) { callbacks = []; this.extensionPoints.set(extensionPointName, callbacks); } callbacks.push(callback); } evaluateExtensionPoint(extensionPointName, extendee, props) { const callbacks = this.extensionPoints.get(extensionPointName); let accumulator = extendee; if (callbacks) { for (const callback of callbacks) { try { accumulator = callback(accumulator, props); } catch (error) { console.error(error); } } } return accumulator; } async evaluateAsyncExtensionPoint(extensionPointName, extendee, props) { const callbacks = this.extensionPoints.get(extensionPointName); let accumulator = extendee; if (callbacks) { for (const callback of callbacks) { try { accumulator = await callback(accumulator, props); } catch (error) { console.error(error); } } } return accumulator; } }