@jbrowse/core
Version:
JBrowse 2 core libraries used by plugins
415 lines (414 loc) • 15.1 kB
JavaScript
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;
}
}