@jbrowse/core 5.0.0-beta.1 → 5.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -3
- package/esm/BaseFeatureWidget/SequenceFeatureDetails/test_data/NCDN.d.ts +2 -2
- package/esm/PluginLoader.d.ts +1 -0
- package/esm/PluginLoader.js +52 -8
- package/esm/PluginManager.d.ts +70 -17
- package/esm/PluginManager.js +151 -20
- package/esm/ReExports/modules.js +3 -53
- package/esm/ReExports/publicUtil.d.ts +1 -1
- package/esm/ReExports/publicUtil.js +1 -1
- package/esm/ReExports/sharedModules.d.ts +96 -0
- package/esm/ReExports/sharedModules.js +59 -0
- package/esm/ReExports/uiStub.d.ts +1 -0
- package/esm/ReExports/uiStub.js +13 -0
- package/esm/ReExports/workerModules.d.ts +2 -0
- package/esm/ReExports/workerModules.js +10 -0
- package/esm/configuration/configurationSchema.d.ts +5 -38
- package/esm/configuration/configurationSchema.js +53 -40
- package/esm/configuration/promotableDefaults.js +12 -6
- package/esm/configuration/promotableResolve.d.ts +1 -1
- package/esm/configuration/promotableResolve.js +1 -1
- package/esm/data_adapters/BaseAdapter/BaseFeatureDataAdapter.d.ts +12 -0
- package/esm/data_adapters/BaseAdapter/BaseFeatureDataAdapter.js +39 -0
- package/esm/data_adapters/BaseAdapter/featureDensity.d.ts +29 -0
- package/esm/data_adapters/BaseAdapter/featureDensity.js +17 -0
- package/esm/data_adapters/BaseAdapter/index.d.ts +1 -0
- package/esm/data_adapters/BaseAdapter/index.js +1 -0
- package/esm/data_adapters/BaseAdapter/stats.d.ts +33 -1
- package/esm/data_adapters/BaseAdapter/stats.js +52 -13
- package/esm/data_adapters/BaseAdapter/types.d.ts +1 -0
- package/esm/pluggableElementTypes/DisplayType.d.ts +9 -10
- package/esm/pluggableElementTypes/DisplayType.js +43 -8
- package/esm/pluggableElementTypes/ViewType.d.ts +25 -2
- package/esm/pluggableElementTypes/ViewType.js +75 -2
- package/esm/pluggableElementTypes/extendElementType.js +8 -3
- package/esm/pluggableElementTypes/models/BaseDisplayModel.d.ts +41 -0
- package/esm/pluggableElementTypes/models/BaseDisplayModel.js +74 -1
- package/esm/pluggableElementTypes/models/BaseTrackModel.d.ts +21 -0
- package/esm/pluggableElementTypes/models/BaseTrackModel.js +37 -2
- package/esm/pluggableElementTypes/models/BaseViewModel.d.ts +49 -0
- package/esm/pluggableElementTypes/models/BaseViewModel.js +39 -0
- package/esm/pluggableElementTypes/models/assertDisplayContract.d.ts +10 -7
- package/esm/pluggableElementTypes/models/assertDisplayContract.js +23 -30
- package/esm/rpc/RpcRegistry.d.ts +10 -0
- package/esm/rpc/byteBudget.d.ts +9 -0
- package/esm/rpc/coreRpcMethods.d.ts +1 -0
- package/esm/rpc/coreRpcMethods.js +1 -0
- package/esm/rpc/methods/CoreGetFeatureDensity.d.ts +11 -0
- package/esm/rpc/methods/CoreGetFeatureDensity.js +22 -0
- package/esm/svg/svgReady.d.ts +1 -0
- package/esm/ui/CascadingMenu.js +30 -8
- package/esm/ui/LoadingOverlay.d.ts +14 -0
- package/esm/ui/LoadingOverlay.js +1 -1
- package/esm/ui/MenuTypes.d.ts +11 -2
- package/esm/ui/MenuTypes.js +3 -0
- package/esm/ui/PaletteContext.d.ts +11 -4
- package/esm/ui/PaletteContext.js +34 -5
- package/esm/ui/PinAdornment.js +12 -6
- package/esm/ui/index.d.ts +1 -1
- package/esm/ui/index.js +1 -1
- package/esm/ui/launchViewMenu.js +7 -1
- package/esm/ui/legendMenuItem.d.ts +5 -5
- package/esm/ui/legendMenuItem.js +5 -5
- package/esm/ui/makeSizeMenu.js +1 -1
- package/esm/ui/menuItems.d.ts +1 -1
- package/esm/ui/menuItems.js +1 -1
- package/esm/ui/promotableMenuItems.js +8 -6
- package/esm/ui/promotablePinCoverage.js +2 -1
- package/esm/ui/toggleMenuItems.d.ts +1 -3
- package/esm/util/addAndShowTrack.d.ts +1 -1
- package/esm/util/addAndShowTrack.js +5 -1
- package/esm/util/addTrackFromWidget.js +2 -2
- package/esm/util/cacheBudgets.d.ts +22 -19
- package/esm/util/cacheBudgets.js +22 -19
- package/esm/util/createStopTokenRotation.d.ts +1 -0
- package/esm/util/createStopTokenRotation.js +7 -27
- package/esm/util/fetchContext.d.ts +2 -2
- package/esm/util/fetchContext.js +2 -2
- package/esm/util/fetchPhases.d.ts +9 -9
- package/esm/util/hooks.d.ts +8 -0
- package/esm/util/hooks.js +27 -0
- package/esm/util/index.d.ts +2 -3
- package/esm/util/index.js +2 -3
- package/esm/util/installFetch.d.ts +49 -23
- package/esm/util/installFetch.js +49 -21
- package/esm/util/installInitAutorun.d.ts +8 -8
- package/esm/util/installInitAutorun.js +15 -15
- package/esm/util/lazyAfterAttach.d.ts +13 -0
- package/esm/util/lazyAfterAttach.js +31 -0
- package/esm/util/legendCandidates.d.ts +50 -0
- package/esm/util/legendCandidates.js +87 -0
- package/esm/util/mstUtils.d.ts +12 -1
- package/esm/util/mstUtils.js +20 -2
- package/esm/util/pluginStore.d.ts +1 -0
- package/esm/util/pluginStore.js +8 -1
- package/esm/util/range.d.ts +13 -9
- package/esm/util/range.js +17 -8
- package/esm/util/rxjs.d.ts +16 -0
- package/esm/util/rxjs.js +49 -0
- package/esm/util/tabix.d.ts +10 -4
- package/esm/util/tabix.js +11 -5
- package/esm/util/tracks.d.ts +48 -0
- package/esm/util/tracks.js +192 -66
- package/esm/util/types/index.d.ts +48 -5
- package/esm/util/types/index.js +3 -0
- package/esm/util/unknownSnapshotKeys.d.ts +36 -0
- package/esm/util/unknownSnapshotKeys.js +69 -0
- package/esm/util/viewStatus.d.ts +23 -0
- package/esm/util/viewStatus.js +17 -0
- package/esm/util/whenViewSettled.d.ts +17 -4
- package/esm/util/whenViewSettled.js +19 -6
- package/esm/util/withLaunchInput.d.ts +123 -0
- package/esm/util/withLaunchInput.js +228 -0
- package/package.json +12 -9
- package/esm/util/applyInitSettings.d.ts +0 -79
- package/esm/util/applyInitSettings.js +0 -91
package/README.md
CHANGED
|
@@ -402,8 +402,11 @@ not be the thing that discovers this. Callers fall back to treating it as the
|
|
|
402
402
|
plain object it is.
|
|
403
403
|
|
|
404
404
|
Shares `TrackConfigurationReference`'s per-PluginManager cache, so hydrating the
|
|
405
|
-
same entry twice returns the same node and
|
|
406
|
-
it
|
|
405
|
+
same entry twice returns the same node — and in admin/embedded sessions a track
|
|
406
|
+
opened later resolves to that same node. A non-admin's open track does not: it
|
|
407
|
+
resolves to the session's private working copy (ADR-032) and this is the
|
|
408
|
+
pristine mirror beside it. The two agree in content, which is what the caller
|
|
409
|
+
needs; `CopyConfigEntryPoints.test.ts` pins both halves.
|
|
407
410
|
|
|
408
411
|
```js
|
|
409
412
|
// type signature
|
|
@@ -432,6 +435,18 @@ the check off (`HostChecksSlotNames`).
|
|
|
432
435
|
|
|
433
436
|
[Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/core/src/configuration/promotableDefaults.ts)
|
|
434
437
|
|
|
438
|
+
### launchOrReplaceView
|
|
439
|
+
|
|
440
|
+
`addOrReplaceView` for view types whose state model may be lazily loaded; the
|
|
441
|
+
synchronous `addOrReplaceView` requires it loaded already.
|
|
442
|
+
|
|
443
|
+
```js
|
|
444
|
+
// type signature
|
|
445
|
+
(args: { session: AbstractViewContainer; typeName: string; initialState?: Record<string, unknown> | undefined; replacing?: AbstractViewModel | undefined; }) => Promise<...>
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
[Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/core/src/util/mstUtils.ts)
|
|
449
|
+
|
|
435
450
|
### makePin
|
|
436
451
|
|
|
437
452
|
The pin for one promotable slot: "apply this value to every open track of this
|
|
@@ -648,6 +663,24 @@ at the point where the connection is cut.
|
|
|
648
663
|
|
|
649
664
|
[Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/core/src/configuration/promotableDefaults.ts)
|
|
650
665
|
|
|
666
|
+
### runLazyAfterAttach
|
|
667
|
+
|
|
668
|
+
Run a display's `afterAttach` body from a module loaded on demand, so the
|
|
669
|
+
autorun installers stay out of the display's eager bundle.
|
|
670
|
+
|
|
671
|
+
One policy for the gap the `await` opens: a node torn down before the module
|
|
672
|
+
lands installs nothing, and a module that fails to load is reported where the
|
|
673
|
+
user can see it rather than onto the display's own error slot — that slot is
|
|
674
|
+
what `reload()` clears, and nothing would re-run the install behind it. Three
|
|
675
|
+
displays hand-rolled this IIFE and each drew the lines differently.
|
|
676
|
+
|
|
677
|
+
```js
|
|
678
|
+
// type signature
|
|
679
|
+
<Self extends IStateTreeNode>(self: Self, load: () => Promise<(self: Self) => void>) => void
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
[Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/core/src/util/lazyAfterAttach.ts)
|
|
683
|
+
|
|
651
684
|
### sampleColorRamp
|
|
652
685
|
|
|
653
686
|
The color at `t` in `[0, 1]` across a list of EVENLY SPACED stops, linearly
|
|
@@ -671,6 +704,12 @@ Make JBrowse follow the host's light/dark state — the whole of it, in one moun
|
|
|
671
704
|
</SessionPaletteProvider>
|
|
672
705
|
```
|
|
673
706
|
|
|
707
|
+
`mode` is optional. Left out, JBrowse follows `prefers-color-scheme` and
|
|
708
|
+
re-themes when the OS preference changes, through the same session write an
|
|
709
|
+
explicit mode takes — so a host whose dark mode _is_ the OS preference mounts
|
|
710
|
+
this with a session and nothing else. Pass a mode as soon as the host has a
|
|
711
|
+
toggle of its own, since the media query cannot see it.
|
|
712
|
+
|
|
674
713
|
A component rather than a documented pair of calls because the pair has a half
|
|
675
714
|
that can be left out with nothing to show for it. `PaletteProvider` is the name
|
|
676
715
|
a host reaches for, and it colors the React side alone; the session write is
|
|
@@ -684,7 +723,7 @@ colors of its own mounts `PaletteProvider` directly instead.
|
|
|
684
723
|
|
|
685
724
|
```js
|
|
686
725
|
// type signature
|
|
687
|
-
({ session, mode, children, }: { session: ThemeModeSession; mode
|
|
726
|
+
({ session, mode, children, }: { session: ThemeModeSession; mode?: "dark" | "light" | undefined; children: ReactNode; }) => Element
|
|
688
727
|
```
|
|
689
728
|
|
|
690
729
|
[Source code](https://github.com/GMOD/jbrowse-components/blob/main/packages/core/src/ui/PaletteContext.tsx)
|
|
@@ -48,9 +48,9 @@ declare const _default: {
|
|
|
48
48
|
uniqueId: string;
|
|
49
49
|
parentId: string;
|
|
50
50
|
id?: undefined;
|
|
51
|
+
name?: undefined;
|
|
51
52
|
note?: undefined;
|
|
52
53
|
protein_id?: undefined;
|
|
53
|
-
name?: undefined;
|
|
54
54
|
} | {
|
|
55
55
|
transcript_id?: undefined;
|
|
56
56
|
source: string;
|
|
@@ -94,6 +94,7 @@ declare const _default: {
|
|
|
94
94
|
transcript_id: string;
|
|
95
95
|
subfeatures: ({
|
|
96
96
|
id?: undefined;
|
|
97
|
+
name?: undefined;
|
|
97
98
|
note?: undefined;
|
|
98
99
|
protein_id?: undefined;
|
|
99
100
|
source: string;
|
|
@@ -111,7 +112,6 @@ declare const _default: {
|
|
|
111
112
|
transcript_id: string;
|
|
112
113
|
uniqueId: string;
|
|
113
114
|
parentId: string;
|
|
114
|
-
name?: undefined;
|
|
115
115
|
} | {
|
|
116
116
|
transcript_id?: undefined;
|
|
117
117
|
source: string;
|
package/esm/PluginLoader.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface PluginLoadFailure {
|
|
|
12
12
|
definition: PluginDefinition;
|
|
13
13
|
error: unknown;
|
|
14
14
|
}
|
|
15
|
+
export declare function workerScriptLoadMessage(scriptUrl: string, error: unknown): string;
|
|
15
16
|
export default class PluginLoader {
|
|
16
17
|
definitions: PluginDefinition[];
|
|
17
18
|
fetchESM?: (url: string) => Promise<LoadedPlugin>;
|
package/esm/PluginLoader.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { enableContractReports } from '@jbrowse/render-core/contractReports';
|
|
1
2
|
import { setReExportRegistry } from './ReExports/registry.js';
|
|
2
|
-
import { isCJSPluginDefinition, isESMPluginDefinition, isUMDPluginDefinition, pluginDescriptionString, } from './pluginDefinitions.js';
|
|
3
|
+
import { isCJSPluginDefinition, isESMPluginDefinition, isUMDPluginDefinition, maybePluginUrl, pluginDescriptionString, } from './pluginDefinitions.js';
|
|
3
4
|
import { isElectron } from './util/index.js';
|
|
4
5
|
import { isWebWorker } from './util/isWebWorker.js';
|
|
5
6
|
/**
|
|
@@ -45,6 +46,19 @@ function promisifiedLoadScript(src, integrity) {
|
|
|
45
46
|
function hasImportScripts(scope) {
|
|
46
47
|
return 'importScripts' in scope;
|
|
47
48
|
}
|
|
49
|
+
// A *module* worker has importScripts on its global but throws from it — the
|
|
50
|
+
// spec forbids it there, and the browser says so ("Module scripts don't support
|
|
51
|
+
// importScripts"). That is the whole failure for a UMD plugin in a bundler
|
|
52
|
+
// configured for module workers (Vite's `worker.format: 'es'`), and it reads as
|
|
53
|
+
// a broken plugin unless named. Every other throw is the script's own — a 404,
|
|
54
|
+
// or a bundle that dereferences `window` in a realm that has none — and the
|
|
55
|
+
// script's words are the diagnosis, so they are kept rather than replaced.
|
|
56
|
+
export function workerScriptLoadMessage(scriptUrl, error) {
|
|
57
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
58
|
+
return /module script/i.test(detail)
|
|
59
|
+
? `Failed to load ${scriptUrl} in the worker. A UMD plugin is loaded there via importScripts, which module workers do not support — either build the worker as a classic worker, or use a plugin published as ESM.`
|
|
60
|
+
: `Failed to load ${scriptUrl} in the worker: ${detail}`;
|
|
61
|
+
}
|
|
48
62
|
async function loadScript(scriptUrl, integrity) {
|
|
49
63
|
const scope = globalThis;
|
|
50
64
|
if (!isWebWorker()) {
|
|
@@ -55,12 +69,9 @@ async function loadScript(scriptUrl, integrity) {
|
|
|
55
69
|
scope.importScripts(scriptUrl);
|
|
56
70
|
}
|
|
57
71
|
catch (error) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// 'es'`), and the raw TypeError names none of it, so it reads as a broken
|
|
62
|
-
// plugin rather than an incompatible pairing.
|
|
63
|
-
throw new Error(`Failed to load ${scriptUrl} in the worker. A UMD plugin is loaded there via importScripts, which module workers do not support — either build the worker as a classic worker, or use a plugin published as ESM.`, { cause: error });
|
|
72
|
+
throw new Error(workerScriptLoadMessage(scriptUrl, error), {
|
|
73
|
+
cause: error,
|
|
74
|
+
});
|
|
64
75
|
}
|
|
65
76
|
return;
|
|
66
77
|
}
|
|
@@ -97,6 +108,34 @@ function resolvePluginUrl(spec, base) {
|
|
|
97
108
|
}
|
|
98
109
|
return url;
|
|
99
110
|
}
|
|
111
|
+
// A plugin served from the developer's own machine into an app built for
|
|
112
|
+
// production is the one arrangement that can only be a plugin under
|
|
113
|
+
// development: a shipped site serves its plugins from its own origin, and a
|
|
114
|
+
// developer running the app itself from source already has a development build.
|
|
115
|
+
// So it is the evidence that arms the contract channel without anything for the
|
|
116
|
+
// author to know about first — which is the whole difficulty the channel had,
|
|
117
|
+
// since the population it is written for is the one with nobody to ask. See
|
|
118
|
+
// `@jbrowse/render-core/contractReports`.
|
|
119
|
+
function armIfUnderDevelopment(def) {
|
|
120
|
+
const url = maybePluginUrl(def);
|
|
121
|
+
if (url !== undefined && servedFromThisMachine(url)) {
|
|
122
|
+
enableContractReports(`the plugin at ${url} is served from this machine`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function servedFromThisMachine(url) {
|
|
126
|
+
try {
|
|
127
|
+
const { hostname, protocol } = new URL(url);
|
|
128
|
+
return ((protocol === 'http:' || protocol === 'https:') &&
|
|
129
|
+
(hostname === 'localhost' ||
|
|
130
|
+
hostname === '127.0.0.1' ||
|
|
131
|
+
hostname === '[::1]'));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// a relative url is a plugin shipped beside this index.html, which is the
|
|
135
|
+
// site's own rather than one under development
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
100
139
|
function addCacheBuster(url) {
|
|
101
140
|
if (!('__jbrowseCacheBuster' in globalThis)) {
|
|
102
141
|
return url;
|
|
@@ -155,6 +194,7 @@ export default class PluginLoader {
|
|
|
155
194
|
}
|
|
156
195
|
async loadPlugin(def, baseUri) {
|
|
157
196
|
assertSingleKind(def);
|
|
197
|
+
armIfUnderDevelopment(def);
|
|
158
198
|
let plugin;
|
|
159
199
|
if (isCJSPluginDefinition(def)) {
|
|
160
200
|
if (!isElectron) {
|
|
@@ -198,7 +238,11 @@ export default class PluginLoader {
|
|
|
198
238
|
if (!target) {
|
|
199
239
|
return;
|
|
200
240
|
}
|
|
201
|
-
|
|
241
|
+
// a worker never renders: the same keys, with the UI ones stubbed, so a
|
|
242
|
+
// plugin bundle evaluates there without fetching react-dom and Material UI
|
|
243
|
+
const { default: ReExports } = isWebWorker()
|
|
244
|
+
? await import('./ReExports/workerModules.js')
|
|
245
|
+
: await import('./ReExports/index.js');
|
|
202
246
|
target.JBrowseExports = {
|
|
203
247
|
...ReExports,
|
|
204
248
|
};
|
package/esm/PluginManager.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ import './ui/buildExtraTrackMenuItems.ts';
|
|
|
20
20
|
import './ui/multiTrackMenuItems.ts';
|
|
21
21
|
import './util/addTrackComponent.ts';
|
|
22
22
|
import type { AbstractRootModel, AbstractSessionModel, SimpleFeatureSerialized } from './util/index.ts';
|
|
23
|
-
import type { IAnyModelType, IStateTreeNode, IAnyType } from '@jbrowse/mobx-state-tree';
|
|
23
|
+
import type { IAnyModelType, IStateTreeNode, IAnyType, SnapshotIn } from '@jbrowse/mobx-state-tree';
|
|
24
24
|
import type { ComponentType, ReactNode } from 'react';
|
|
25
25
|
/**
|
|
26
26
|
* Every pluggable element group, in the order `elementCreationSchedule` builds
|
|
@@ -137,7 +137,18 @@ export interface ReplaceWidgetProps {
|
|
|
137
137
|
*
|
|
138
138
|
* That is what makes `extendViewType(pm, 'LinearGenomeView', …)` hand back a
|
|
139
139
|
* typed state model, and a misspelled or renamed name a compile error rather
|
|
140
|
-
* than an extension that quietly never applies.
|
|
140
|
+
* than an extension that quietly never applies. `getViewType` reads it too, so
|
|
141
|
+
* a caller composing another view's model into its own gets the real type
|
|
142
|
+
* instead of `IAnyModelType`.
|
|
143
|
+
*
|
|
144
|
+
* AN AUGMENTATION IS HAND-WRITTEN, so each registration annotates the model it
|
|
145
|
+
* passes to `ViewType` against its own entry here (`const stateModel:
|
|
146
|
+
* ViewTypeRegistry['CircularView'] = …`). That one line is what makes the
|
|
147
|
+
* registry earn what `getViewType` promises: without it a drifted entry is
|
|
148
|
+
* worse than the `IAnyModelType` it replaced, since a caller would get a
|
|
149
|
+
* confidently wrong type rather than an unchecked one. It cannot live on
|
|
150
|
+
* `ViewType`'s constructor — TypeScript forbids type parameters there, and a
|
|
151
|
+
* class parameter widens `name` to `string`, so the conditional never fires. Declaring them here rather than
|
|
141
152
|
* beside the helpers is load-bearing: an interface can only be augmented
|
|
142
153
|
* through the module that declares it, and a barrel re-exporting the type would
|
|
143
154
|
* give every augmentation its own unrelated copy.
|
|
@@ -148,6 +159,21 @@ export interface DisplayTypeRegistry {
|
|
|
148
159
|
}
|
|
149
160
|
export type ViewTypeName = keyof ViewTypeRegistry;
|
|
150
161
|
export type DisplayTypeName = keyof DisplayTypeRegistry;
|
|
162
|
+
/**
|
|
163
|
+
* What `addView(name, snapshot)` takes for a view type name. A registered name
|
|
164
|
+
* gets the view's authored shape — the snapshot type `withLaunchInput` widened
|
|
165
|
+
* to accept the launch keys beside the declared properties — so a misspelled
|
|
166
|
+
* key is a compile error at the literal site. `type` is optional because
|
|
167
|
+
* `addView` writes it. An unregistered name stays `Record<string, unknown>`,
|
|
168
|
+
* which is what an out-of-tree view's `addView` has always been.
|
|
169
|
+
*
|
|
170
|
+
* Read off `ViewTypeRegistry` rather than registered a second time, so the
|
|
171
|
+
* snapshot type cannot drift from the model: a view that augments the registry
|
|
172
|
+
* has typed `addView`, `replaceView` and `addOrReplaceView` for free.
|
|
173
|
+
*/
|
|
174
|
+
export type ViewSnapshotInput<N extends string> = N extends ViewTypeName ? Omit<SnapshotIn<ViewTypeRegistry[N]>, 'type'> & {
|
|
175
|
+
type?: N;
|
|
176
|
+
} : Record<string, unknown>;
|
|
151
177
|
export interface ExtensionPointRegistry {
|
|
152
178
|
'Core-extendSession': {
|
|
153
179
|
args: IAnyModelType;
|
|
@@ -268,6 +294,7 @@ export interface RuntimePluginLoadRecord extends PluginLoadRecord {
|
|
|
268
294
|
definition: PluginDefinition;
|
|
269
295
|
}
|
|
270
296
|
export default class PluginManager {
|
|
297
|
+
#private;
|
|
271
298
|
plugins: Plugin[];
|
|
272
299
|
jexl: import("@jbrowse/jexl").Jexl;
|
|
273
300
|
pluginMetadata: Record<string, PluginMetadata>;
|
|
@@ -289,25 +316,27 @@ export default class PluginManager {
|
|
|
289
316
|
extensionPoints: Map<string, ExtensionPointCallback[]>;
|
|
290
317
|
warnedClobber: Set<string>;
|
|
291
318
|
/**
|
|
292
|
-
*
|
|
293
|
-
* `
|
|
319
|
+
* The lazily-hydrated MST config node for one frozen track config, built by
|
|
320
|
+
* `build` on first ask and returned by identity after that.
|
|
321
|
+
* `configuration/configurationSchema.ts` is the only caller.
|
|
322
|
+
*
|
|
294
323
|
* `jbrowse.tracks` is `types.frozen` for large-tracklist performance, so a
|
|
295
|
-
* track config is a plain JS object until first referenced; hydrating it
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
* See ADR-031.
|
|
324
|
+
* track config is a plain JS object until first referenced; hydrating it into
|
|
325
|
+
* an MST node is deferred to that read. MST's custom-reference `getValue` has
|
|
326
|
+
* no memoization of its own — it reruns on every property access — so without
|
|
327
|
+
* this, every read of `track.configuration` would fabricate a fresh,
|
|
328
|
+
* non-identical node. Keyed by schemaType (each track type's config schema is
|
|
329
|
+
* rebuilt fresh per PluginManager instance, see addTrackType) then by the
|
|
330
|
+
* frozen object, so a hit can only ever come from this same instance and this
|
|
331
|
+
* same track type. See ADR-031.
|
|
304
332
|
*
|
|
305
|
-
*
|
|
333
|
+
* The node is never mutated: admin edits replace the frozen entry (new
|
|
306
334
|
* identity drops the WeakMap entry), and a non-admin's edits go to a private
|
|
307
335
|
* session working copy, not here (ADR-032). Both levels are `WeakMap`s so
|
|
308
|
-
* entries collect normally — no manual invalidation needed
|
|
336
|
+
* entries collect normally — no manual invalidation needed, which is why this
|
|
337
|
+
* is a memo and not a cache with a lifecycle to expose.
|
|
309
338
|
*/
|
|
310
|
-
|
|
339
|
+
hydratedTrackConfig(schemaType: IAnyType, frozen: object, build: () => unknown): unknown;
|
|
311
340
|
constructor(initialPlugins?: (Plugin | PluginLoadRecord | RuntimePluginLoadRecord)[]);
|
|
312
341
|
pluginConfigurationNamespacedSchemas(): Record<string, unknown>;
|
|
313
342
|
pluginConfigurationUnnamespacedSchemas(): Record<string, unknown>;
|
|
@@ -330,7 +359,11 @@ export default class PluginManager {
|
|
|
330
359
|
getRpcElements(): RpcMethodType[];
|
|
331
360
|
getDisplayElements(): DisplayType[];
|
|
332
361
|
getAdapterElements(): AdapterType[];
|
|
333
|
-
/**
|
|
362
|
+
/**
|
|
363
|
+
* get a MST type for the union of all specified pluggable MST types.
|
|
364
|
+
* Membership is late-bound, so a lazily registered state model joins when
|
|
365
|
+
* its loader resolves; a snapshot naming one cannot be cast until then.
|
|
366
|
+
*/
|
|
334
367
|
pluggableMstType(groupName: PluggableElementTypeGroup, fieldName: string, fallback?: IAnyType): IAnyType;
|
|
335
368
|
/** get a MST type for the union of all specified pluggable config schemas */
|
|
336
369
|
pluggableConfigSchemaType(typeGroup: PluggableElementTypeGroup, fieldName?: string): IAnyType;
|
|
@@ -357,7 +390,27 @@ export default class PluginManager {
|
|
|
357
390
|
getTextSearchAdapterType(typeName: string): TextSearchAdapterType;
|
|
358
391
|
getTrackType(typeName: string): TrackType;
|
|
359
392
|
getDisplayType(typeName: string): DisplayType;
|
|
393
|
+
getViewType<N extends ViewTypeName>(typeName: N): Omit<ViewType, 'stateModel'> & {
|
|
394
|
+
stateModel: ViewTypeRegistry[N];
|
|
395
|
+
};
|
|
360
396
|
getViewType(typeName: string): ViewType;
|
|
397
|
+
private sessionTypeNames;
|
|
398
|
+
/**
|
|
399
|
+
* Every async code path that hands a snapshot to a synchronous session
|
|
400
|
+
* instantiation (`setSession`, `cast`) must await this first.
|
|
401
|
+
*/
|
|
402
|
+
preloadSessionTypes(sessionSnapshot: unknown): Promise<void>;
|
|
403
|
+
unloadedSessionTypes(sessionSnapshot: unknown): string[];
|
|
404
|
+
/**
|
|
405
|
+
* Without this, a snapshot naming an unloaded lazy type fails inside MST as
|
|
406
|
+
* a union mismatch that reads like a corrupt snapshot.
|
|
407
|
+
*/
|
|
408
|
+
assertSessionTypesLoaded(sessionSnapshot: unknown): void;
|
|
409
|
+
/**
|
|
410
|
+
* Aliases resolve here because a legacy session names the alias, and the
|
|
411
|
+
* model that remaps it cannot be consulted before it is loaded.
|
|
412
|
+
*/
|
|
413
|
+
resolveDisplayTypeRecord(name: string): DisplayType | undefined;
|
|
361
414
|
getAddTrackWorkflow(typeName: string): AddTrackWorkflowType;
|
|
362
415
|
getWidgetType(typeName: string): WidgetType;
|
|
363
416
|
getConnectionType(typeName: string): ConnectionType;
|
package/esm/PluginManager.js
CHANGED
|
@@ -113,26 +113,41 @@ export default class PluginManager {
|
|
|
113
113
|
// singular component points are re-evaluated on every render, so the
|
|
114
114
|
// clobber warning is emitted once per point per app
|
|
115
115
|
warnedClobber = new Set();
|
|
116
|
+
#trackConfigHydrationCache = new WeakMap();
|
|
116
117
|
/**
|
|
117
|
-
*
|
|
118
|
-
* `
|
|
118
|
+
* The lazily-hydrated MST config node for one frozen track config, built by
|
|
119
|
+
* `build` on first ask and returned by identity after that.
|
|
120
|
+
* `configuration/configurationSchema.ts` is the only caller.
|
|
121
|
+
*
|
|
119
122
|
* `jbrowse.tracks` is `types.frozen` for large-tracklist performance, so a
|
|
120
|
-
* track config is a plain JS object until first referenced; hydrating it
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* See ADR-031.
|
|
123
|
+
* track config is a plain JS object until first referenced; hydrating it into
|
|
124
|
+
* an MST node is deferred to that read. MST's custom-reference `getValue` has
|
|
125
|
+
* no memoization of its own — it reruns on every property access — so without
|
|
126
|
+
* this, every read of `track.configuration` would fabricate a fresh,
|
|
127
|
+
* non-identical node. Keyed by schemaType (each track type's config schema is
|
|
128
|
+
* rebuilt fresh per PluginManager instance, see addTrackType) then by the
|
|
129
|
+
* frozen object, so a hit can only ever come from this same instance and this
|
|
130
|
+
* same track type. See ADR-031.
|
|
129
131
|
*
|
|
130
|
-
*
|
|
132
|
+
* The node is never mutated: admin edits replace the frozen entry (new
|
|
131
133
|
* identity drops the WeakMap entry), and a non-admin's edits go to a private
|
|
132
134
|
* session working copy, not here (ADR-032). Both levels are `WeakMap`s so
|
|
133
|
-
* entries collect normally — no manual invalidation needed
|
|
135
|
+
* entries collect normally — no manual invalidation needed, which is why this
|
|
136
|
+
* is a memo and not a cache with a lifecycle to expose.
|
|
134
137
|
*/
|
|
135
|
-
|
|
138
|
+
hydratedTrackConfig(schemaType, frozen, build) {
|
|
139
|
+
let byFrozen = this.#trackConfigHydrationCache.get(schemaType);
|
|
140
|
+
if (!byFrozen) {
|
|
141
|
+
byFrozen = new WeakMap();
|
|
142
|
+
this.#trackConfigHydrationCache.set(schemaType, byFrozen);
|
|
143
|
+
}
|
|
144
|
+
let node = byFrozen.get(frozen);
|
|
145
|
+
if (!node) {
|
|
146
|
+
node = build();
|
|
147
|
+
byFrozen.set(frozen, node);
|
|
148
|
+
}
|
|
149
|
+
return node;
|
|
150
|
+
}
|
|
136
151
|
constructor(initialPlugins = []) {
|
|
137
152
|
// add the core plugin
|
|
138
153
|
this.addPlugin({
|
|
@@ -326,16 +341,36 @@ export default class PluginManager {
|
|
|
326
341
|
getAdapterElements() {
|
|
327
342
|
return this.getElementTypesInGroup('adapter');
|
|
328
343
|
}
|
|
329
|
-
/**
|
|
344
|
+
/**
|
|
345
|
+
* get a MST type for the union of all specified pluggable MST types.
|
|
346
|
+
* Membership is late-bound, so a lazily registered state model joins when
|
|
347
|
+
* its loader resolves; a snapshot naming one cannot be cast until then.
|
|
348
|
+
*/
|
|
330
349
|
pluggableMstType(groupName, fieldName, fallback = types.maybe(types.null)) {
|
|
331
|
-
const
|
|
350
|
+
const record = this.getElementTypeRecord(groupName);
|
|
351
|
+
const anyMember = record.all().some(t => {
|
|
352
|
+
const element = t;
|
|
353
|
+
const field = element[fieldName];
|
|
354
|
+
return ((isType(field) && isModelType(field)) ||
|
|
355
|
+
typeof element[`${fieldName}Loader`] === 'function');
|
|
356
|
+
});
|
|
357
|
+
if (!anyMember) {
|
|
358
|
+
return fallback;
|
|
359
|
+
}
|
|
360
|
+
const modelsOf = () => record
|
|
332
361
|
.all()
|
|
333
362
|
.map(t => t[fieldName])
|
|
334
363
|
.filter(t => isType(t) && isModelType(t));
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
364
|
+
let members = modelsOf();
|
|
365
|
+
return types.union({
|
|
366
|
+
name: `pluggable(${groupName} ${fieldName})`,
|
|
367
|
+
members: () => {
|
|
368
|
+
if (members.length !== modelsOf().length) {
|
|
369
|
+
members = modelsOf();
|
|
370
|
+
}
|
|
371
|
+
return members;
|
|
372
|
+
},
|
|
373
|
+
});
|
|
339
374
|
}
|
|
340
375
|
/** get a MST type for the union of all specified pluggable config schemas */
|
|
341
376
|
pluggableConfigSchemaType(typeGroup, fieldName = 'configSchema') {
|
|
@@ -422,6 +457,102 @@ export default class PluginManager {
|
|
|
422
457
|
getViewType(typeName) {
|
|
423
458
|
return this.viewTypes.get(typeName);
|
|
424
459
|
}
|
|
460
|
+
sessionTypeNames(sessionSnapshot) {
|
|
461
|
+
const viewNames = new Set();
|
|
462
|
+
const displayNames = new Set();
|
|
463
|
+
const collectTracks = (tracks) => {
|
|
464
|
+
if (Array.isArray(tracks)) {
|
|
465
|
+
for (const track of tracks) {
|
|
466
|
+
const { displays } = track && typeof track === 'object'
|
|
467
|
+
? track
|
|
468
|
+
: {};
|
|
469
|
+
if (Array.isArray(displays)) {
|
|
470
|
+
for (const display of displays) {
|
|
471
|
+
const type = display && typeof display === 'object'
|
|
472
|
+
? display.type
|
|
473
|
+
: undefined;
|
|
474
|
+
if (typeof type === 'string') {
|
|
475
|
+
displayNames.add(type);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
const collectViews = (views) => {
|
|
483
|
+
if (Array.isArray(views)) {
|
|
484
|
+
for (const view of views) {
|
|
485
|
+
if (view && typeof view === 'object') {
|
|
486
|
+
const { type, views: children, tracks, levels, } = view;
|
|
487
|
+
if (typeof type === 'string') {
|
|
488
|
+
viewNames.add(type);
|
|
489
|
+
}
|
|
490
|
+
collectViews(children);
|
|
491
|
+
collectTracks(tracks);
|
|
492
|
+
if (Array.isArray(levels)) {
|
|
493
|
+
for (const level of levels) {
|
|
494
|
+
collectTracks(level && typeof level === 'object'
|
|
495
|
+
? level.tracks
|
|
496
|
+
: undefined);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
if (sessionSnapshot && typeof sessionSnapshot === 'object') {
|
|
504
|
+
const { view, views } = sessionSnapshot;
|
|
505
|
+
collectViews(views);
|
|
506
|
+
collectViews(view ? [view] : undefined);
|
|
507
|
+
}
|
|
508
|
+
return { viewNames, displayNames };
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Every async code path that hands a snapshot to a synchronous session
|
|
512
|
+
* instantiation (`setSession`, `cast`) must await this first.
|
|
513
|
+
*/
|
|
514
|
+
async preloadSessionTypes(sessionSnapshot) {
|
|
515
|
+
const { viewNames, displayNames } = this.sessionTypeNames(sessionSnapshot);
|
|
516
|
+
await Promise.all([
|
|
517
|
+
...[...viewNames]
|
|
518
|
+
.filter(name => this.viewTypes.has(name))
|
|
519
|
+
.map(name => this.getViewType(name).loadStateModel()),
|
|
520
|
+
...[...displayNames]
|
|
521
|
+
.map(name => this.resolveDisplayTypeRecord(name))
|
|
522
|
+
.flatMap(display => (display ? [display.loadStateModel()] : [])),
|
|
523
|
+
]);
|
|
524
|
+
}
|
|
525
|
+
unloadedSessionTypes(sessionSnapshot) {
|
|
526
|
+
const { viewNames, displayNames } = this.sessionTypeNames(sessionSnapshot);
|
|
527
|
+
return [
|
|
528
|
+
...[...viewNames].filter(name => this.viewTypes.has(name) &&
|
|
529
|
+
!this.getViewType(name).isStateModelLoaded),
|
|
530
|
+
...[...displayNames].filter(name => {
|
|
531
|
+
const display = this.resolveDisplayTypeRecord(name);
|
|
532
|
+
return display !== undefined && !display.isStateModelLoaded;
|
|
533
|
+
}),
|
|
534
|
+
];
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Without this, a snapshot naming an unloaded lazy type fails inside MST as
|
|
538
|
+
* a union mismatch that reads like a corrupt snapshot.
|
|
539
|
+
*/
|
|
540
|
+
assertSessionTypesLoaded(sessionSnapshot) {
|
|
541
|
+
const unloaded = this.unloadedSessionTypes(sessionSnapshot);
|
|
542
|
+
if (unloaded.length > 0) {
|
|
543
|
+
throw new Error(`session names lazily loaded types that are not loaded yet: ${unloaded.join(', ')}. Await pluginManager.preloadSessionTypes(snapshot) before setting the session`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Aliases resolve here because a legacy session names the alias, and the
|
|
548
|
+
* model that remaps it cannot be consulted before it is loaded.
|
|
549
|
+
*/
|
|
550
|
+
resolveDisplayTypeRecord(name) {
|
|
551
|
+
if (this.displayTypes.has(name)) {
|
|
552
|
+
return this.getDisplayType(name);
|
|
553
|
+
}
|
|
554
|
+
return this.getDisplayElements().find(d => d.aliases?.includes(name));
|
|
555
|
+
}
|
|
425
556
|
getAddTrackWorkflow(typeName) {
|
|
426
557
|
return this.addTrackWidgets.get(typeName);
|
|
427
558
|
}
|