@jbrowse/app-core 4.3.0 → 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/esm/AppSession/AppRootModel.d.ts +30 -0
- package/esm/AppSession/AppRootModel.js +1 -0
- package/esm/AppSession/AppSessionMixin.d.ts +1598 -0
- package/esm/AppSession/AppSessionMixin.js +62 -0
- package/esm/AppSession/index.d.ts +2 -0
- package/esm/AppSession/index.js +1 -0
- package/esm/Assemblies/AssembliesMixin.d.ts +225 -0
- package/esm/Assemblies/AssembliesMixin.js +149 -0
- package/esm/Assemblies/index.d.ts +1 -2
- package/esm/Assemblies/index.js +1 -2
- package/esm/HistoryManagement/index.d.ts +19 -0
- package/esm/HistoryManagement/index.js +42 -5
- package/esm/JBrowseConfig/RootConfiguration.d.ts +171 -78
- package/esm/JBrowseConfig/RootConfiguration.js +85 -7
- package/esm/JBrowseConfig/index.d.ts +173 -84
- package/esm/JBrowseConfig/index.js +62 -2
- package/esm/JBrowseModel/index.d.ts +180 -95
- package/esm/JBrowseModel/index.js +122 -21
- package/esm/JbApi/index.d.ts +2 -0
- package/esm/JbApi/index.js +1 -0
- package/esm/JbApi/jbApi.d.ts +325 -0
- package/esm/JbApi/jbApi.js +748 -0
- package/esm/RootMenu/index.d.ts +69 -3
- package/esm/RootMenu/index.js +96 -61
- package/esm/SessionSpec/index.d.ts +6 -0
- package/esm/SessionSpec/index.js +3 -0
- package/esm/SessionSpec/lgvUrlInit.d.ts +79 -0
- package/esm/SessionSpec/lgvUrlInit.js +158 -0
- package/esm/SessionSpec/loadSessionSpec.d.ts +21 -0
- package/esm/SessionSpec/loadSessionSpec.js +392 -0
- package/esm/SessionSpec/parseSessionSpecUrl.d.ts +13 -0
- package/esm/SessionSpec/parseSessionSpecUrl.js +196 -0
- package/esm/SessionSpec/types.d.ts +31 -0
- package/esm/SessionSpec/types.js +1 -0
- package/esm/WorkspaceLayout/LayoutRenderer.d.ts +24 -0
- package/esm/WorkspaceLayout/LayoutRenderer.js +185 -0
- package/esm/WorkspaceLayout/PanelView.d.ts +13 -0
- package/esm/WorkspaceLayout/PanelView.js +99 -0
- package/esm/WorkspaceLayout/TabStrip.d.ts +14 -0
- package/esm/WorkspaceLayout/TabStrip.js +226 -0
- package/esm/WorkspaceLayout/WorkspaceContainer.d.ts +16 -0
- package/esm/WorkspaceLayout/WorkspaceContainer.js +105 -0
- package/esm/WorkspaceLayout/WorkspacePanelActions.d.ts +24 -0
- package/esm/WorkspaceLayout/WorkspacePanelActions.js +120 -0
- package/esm/WorkspaceLayout/WorkspaceTab.d.ts +27 -0
- package/esm/WorkspaceLayout/WorkspaceTab.js +109 -0
- package/esm/WorkspaceLayout/dockviewTheme.d.ts +69 -0
- package/esm/WorkspaceLayout/dockviewTheme.js +82 -0
- package/esm/WorkspaceLayout/dropZone.d.ts +105 -0
- package/esm/WorkspaceLayout/dropZone.js +113 -0
- package/esm/WorkspaceLayout/index.d.ts +8 -0
- package/esm/WorkspaceLayout/index.js +4 -0
- package/esm/WorkspaceLayout/model.d.ts +248 -0
- package/esm/WorkspaceLayout/model.js +562 -0
- package/esm/WorkspaceLayout/panelChrome.d.ts +19 -0
- package/esm/WorkspaceLayout/panelChrome.js +4 -0
- package/esm/WorkspaceLayout/spec.d.ts +88 -0
- package/esm/WorkspaceLayout/spec.js +226 -0
- package/esm/WorkspaceLayout/splitter.d.ts +25 -0
- package/esm/WorkspaceLayout/splitter.js +37 -0
- package/esm/WorkspaceLayout/tree.d.ts +158 -0
- package/esm/WorkspaceLayout/tree.js +379 -0
- package/esm/WorkspaceLayout/useLayoutDrag.d.ts +34 -0
- package/esm/WorkspaceLayout/useLayoutDrag.js +232 -0
- package/esm/index.d.ts +18 -9
- package/esm/index.js +16 -9
- package/esm/menus.d.ts +55 -68
- package/esm/menus.js +117 -86
- package/esm/queryParams.d.ts +9 -0
- package/esm/queryParams.js +92 -0
- package/esm/sessionModelFactory.d.ts +18 -0
- package/esm/sessionModelFactory.js +1 -0
- package/esm/ui/App/App.d.ts +3 -14
- package/esm/ui/App/App.js +30 -17
- package/esm/ui/App/AppFab.d.ts +12 -3
- package/esm/ui/App/AppFab.js +15 -8
- package/esm/ui/App/AppReadyMarker.d.ts +43 -0
- package/esm/ui/App/AppReadyMarker.js +56 -0
- package/esm/ui/App/AppToolbar.d.ts +2 -14
- package/esm/ui/App/AppToolbar.js +23 -24
- package/esm/ui/App/ClassicViewsContainer.d.ts +3 -9
- package/esm/ui/App/ClassicViewsContainer.js +9 -8
- package/esm/ui/App/DialogQueue.d.ts +17 -1
- package/esm/ui/App/DialogQueue.js +30 -1
- package/esm/ui/App/Drawer.d.ts +1 -1
- package/esm/ui/App/Drawer.js +10 -39
- package/esm/ui/App/DrawerControls.d.ts +1 -5
- package/esm/ui/App/DrawerControls.js +1 -25
- package/esm/ui/App/DrawerHeader.d.ts +2 -3
- package/esm/ui/App/DrawerHeader.js +24 -10
- package/esm/ui/App/DrawerHeaderHelpButton.d.ts +1 -1
- package/esm/ui/App/DrawerHeaderHelpButton.js +3 -3
- package/esm/ui/App/DrawerHeaderHelpDialog.d.ts +2 -2
- package/esm/ui/App/DrawerHeaderHelpDialog.js +3 -4
- package/esm/ui/App/DrawerWidget.d.ts +1 -1
- package/esm/ui/App/DrawerWidget.js +9 -21
- package/esm/ui/App/JBrowseTabMenu.d.ts +2 -3
- package/esm/ui/App/JBrowseTabMenu.js +24 -5
- package/esm/ui/App/ViewButtons.d.ts +2 -4
- package/esm/ui/App/ViewButtons.js +15 -3
- package/esm/ui/App/ViewContainer.d.ts +3 -2
- package/esm/ui/App/ViewContainer.js +144 -24
- package/esm/ui/App/ViewContainerTitle.d.ts +1 -1
- package/esm/ui/App/ViewContainerTitle.js +9 -2
- package/esm/ui/App/ViewErrorFallback.d.ts +18 -0
- package/esm/ui/App/ViewErrorFallback.js +42 -0
- package/esm/ui/App/ViewHeader.d.ts +3 -4
- package/esm/ui/App/ViewHeader.js +28 -17
- package/esm/ui/App/ViewLauncher.d.ts +2 -8
- package/esm/ui/App/ViewLauncher.js +7 -5
- package/esm/ui/App/ViewMenu.d.ts +3 -5
- package/esm/ui/App/ViewMenu.js +87 -92
- package/esm/ui/App/ViewStack.d.ts +7 -0
- package/esm/ui/App/ViewStack.js +42 -0
- package/esm/ui/App/ViewWrapper.d.ts +15 -1
- package/esm/ui/App/ViewWrapper.js +26 -10
- package/esm/ui/App/ViewsContainer.d.ts +3 -8
- package/esm/ui/App/ViewsContainer.js +9 -6
- package/esm/ui/App/copyView.js +1 -1
- package/esm/ui/App/index.d.ts +2 -1
- package/esm/ui/App/index.js +3 -2
- package/esm/ui/App/lazyChunk.d.ts +13 -0
- package/esm/ui/App/lazyChunk.js +91 -0
- package/esm/ui/App/types.d.ts +12 -3
- package/esm/ui/App/types.js +1 -1
- package/esm/ui/App/useViewVisibility.d.ts +34 -0
- package/esm/ui/App/useViewVisibility.js +87 -0
- package/esm/ui/App/viewTitle.d.ts +5 -0
- package/esm/ui/App/viewTitle.js +9 -0
- package/esm/ui/index.d.ts +1 -1
- package/esm/ui/index.js +1 -1
- package/package.json +12 -17
- package/esm/AppFocus/index.d.ts +0 -5
- package/esm/AppFocus/index.js +0 -12
- package/esm/Assemblies/SessionAssembliesMixin.d.ts +0 -92
- package/esm/Assemblies/SessionAssembliesMixin.js +0 -53
- package/esm/Assemblies/TemporaryAssembliesMixin.d.ts +0 -10
- package/esm/Assemblies/TemporaryAssembliesMixin.js +0 -37
- package/esm/DockviewLayout/index.d.ts +0 -35
- package/esm/DockviewLayout/index.js +0 -111
- package/esm/ui/App/DockviewContext.d.ts +0 -22
- package/esm/ui/App/DockviewContext.js +0 -29
- package/esm/ui/App/DockviewLeftHeaderActions.d.ts +0 -2
- package/esm/ui/App/DockviewLeftHeaderActions.js +0 -58
- package/esm/ui/App/DockviewRightHeaderActions.d.ts +0 -2
- package/esm/ui/App/DockviewRightHeaderActions.js +0 -100
- package/esm/ui/App/DrawerWidgetSelector.d.ts +0 -5
- package/esm/ui/App/DrawerWidgetSelector.js +0 -47
- package/esm/ui/App/JBrowseViewPanel.d.ts +0 -8
- package/esm/ui/App/JBrowseViewPanel.js +0 -40
- package/esm/ui/App/JBrowseViewTab.d.ts +0 -8
- package/esm/ui/App/JBrowseViewTab.js +0 -97
- package/esm/ui/App/ModalWidget.d.ts +0 -6
- package/esm/ui/App/ModalWidget.js +0 -48
- package/esm/ui/App/TiledViewsContainer.d.ts +0 -6
- package/esm/ui/App/TiledViewsContainer.js +0 -366
- package/esm/ui/App/dockviewUtils.d.ts +0 -46
- package/esm/ui/App/dockviewUtils.js +0 -55
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
import { setReExportRegistry } from '@jbrowse/core/ReExports/registry';
|
|
2
|
+
import { getConf, getConfigurationSchemaDefinition, isSlotDefinitionEntry, readConfObject, } from '@jbrowse/core/configuration';
|
|
3
|
+
import { getFeatureAdapterOrThrow } from '@jbrowse/core/data_adapters/getFeatureAdapter';
|
|
4
|
+
import { adapterByteLimit, measureRegionBytes, } from '@jbrowse/core/rpc/byteBudget';
|
|
5
|
+
import { getRpcSessionId, isElectron, isSessionWithAddSessionTrack, objectHash, parseLocString, renameRegionsIfNeeded, } from '@jbrowse/core/util';
|
|
6
|
+
import { createStopToken, stopStopToken } from '@jbrowse/core/util/stopToken';
|
|
7
|
+
import { allSessionTracks, getConfAssemblyNamesOrNone, guessTrackConfForLocation, isSameAssemblyName, viewCanDisplayTrack, viewDisplayNames, } from '@jbrowse/core/util/tracks';
|
|
8
|
+
import * as mst from '@jbrowse/mobx-state-tree';
|
|
9
|
+
import { getSnapshot, getType, isStateTreeNode } from '@jbrowse/mobx-state-tree';
|
|
10
|
+
import * as mobx from 'mobx';
|
|
11
|
+
// relative, not '@jbrowse/app-core': a package self-import would make this
|
|
12
|
+
// module depend on the barrel that exports it
|
|
13
|
+
import { loadSessionSpec } from '../SessionSpec/index.js';
|
|
14
|
+
// Matches BaseTrackModel's DEFAULT_EXPORT_BYTE_LIMIT: both bound a fetch
|
|
15
|
+
// someone asked for by name rather than one a view scrolled into, so they
|
|
16
|
+
// should refuse at the same size.
|
|
17
|
+
const DEFAULT_AGENT_BYTE_LIMIT = 5_000_000;
|
|
18
|
+
/**
|
|
19
|
+
* What jb.getFeatures may pull before it refuses. An explicit request wins so
|
|
20
|
+
* an agent can mean it; otherwise the adapter's own declared limit, so this
|
|
21
|
+
* does not disagree with the size the track's display already refuses to
|
|
22
|
+
* render; otherwise the default.
|
|
23
|
+
*/
|
|
24
|
+
export function agentByteLimit(declaredFetchSizeLimit, requested) {
|
|
25
|
+
return (requested ??
|
|
26
|
+
adapterByteLimit(declaredFetchSizeLimit, DEFAULT_AGENT_BYTE_LIMIT));
|
|
27
|
+
}
|
|
28
|
+
function viewSelf(view) {
|
|
29
|
+
return view;
|
|
30
|
+
}
|
|
31
|
+
// The views' own census contract (BaseViewModel derives `allViews` and
|
|
32
|
+
// `ownTracks`), so this module carries no copy of the view nesting: a synteny
|
|
33
|
+
// stack's rows and per-band containers, a breakpoint split view's panels, and
|
|
34
|
+
// any depth of nesting all arrive through each view answering for itself.
|
|
35
|
+
function allViews(session) {
|
|
36
|
+
return session.views.flatMap(v => v.allViews);
|
|
37
|
+
}
|
|
38
|
+
function viewTracks(view) {
|
|
39
|
+
return view.ownTracks;
|
|
40
|
+
}
|
|
41
|
+
function allTracks(session) {
|
|
42
|
+
return allViews(session).flatMap(v => viewTracks(v));
|
|
43
|
+
}
|
|
44
|
+
// The drawn display's on-screen state. `displayPhase` is the tree's own
|
|
45
|
+
// single-sourced vocabulary (render-core/displayPhase.ts) and the same one
|
|
46
|
+
// AppReadyMarker reads; `tooLarge` and `renderError` replace the display
|
|
47
|
+
// subtree rather than raising a snackbar, so a settle reporting only
|
|
48
|
+
// notifications calls them ready.
|
|
49
|
+
function displayState(track) {
|
|
50
|
+
const display = track.displays?.[0];
|
|
51
|
+
if (!display) {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const phase = display.displayPhase;
|
|
56
|
+
return {
|
|
57
|
+
display: display.type,
|
|
58
|
+
// "make it all fit in the window" is a normal thing to be asked, and
|
|
59
|
+
// without this the only way to answer it is measuring DOM rectangles
|
|
60
|
+
...(typeof display.height === 'number' ? { height: display.height } : {}),
|
|
61
|
+
...(phase === undefined || phase === 'ready' ? {} : { phase }),
|
|
62
|
+
...(phase === 'tooLarge' && display.regionTooLargeReason
|
|
63
|
+
? { reason: display.regionTooLargeReason }
|
|
64
|
+
: {}),
|
|
65
|
+
...(display.error ? { error: String(display.error) } : {}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
return { display: display.type, error: String(e) };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function sessionOf(pluginManager) {
|
|
73
|
+
return pluginManager?.rootModel?.session;
|
|
74
|
+
}
|
|
75
|
+
function viewSummary(view) {
|
|
76
|
+
const v = viewSelf(view);
|
|
77
|
+
return {
|
|
78
|
+
id: v.id,
|
|
79
|
+
type: v.type,
|
|
80
|
+
...(v.displayName ? { displayName: v.displayName } : {}),
|
|
81
|
+
...(v.assemblyNames?.length ? { assemblyNames: v.assemblyNames } : {}),
|
|
82
|
+
...(v.coarseVisibleLocStrings
|
|
83
|
+
? { visibleRegion: v.coarseVisibleLocStrings }
|
|
84
|
+
: {}),
|
|
85
|
+
// the stack's own height, beside each track's below it: "does this all fit
|
|
86
|
+
// on screen" is then arithmetic rather than three rounds of screenshotting
|
|
87
|
+
...(typeof v.height === 'number' ? { height: v.height } : {}),
|
|
88
|
+
...(viewTracks(view).length
|
|
89
|
+
? {
|
|
90
|
+
tracks: viewTracks(view).map(t => ({
|
|
91
|
+
trackId: t.configuration.trackId,
|
|
92
|
+
trackType: t.type,
|
|
93
|
+
...displayState(t),
|
|
94
|
+
})),
|
|
95
|
+
}
|
|
96
|
+
: {}),
|
|
97
|
+
...(Array.isArray(v.views) && v.views.length
|
|
98
|
+
? { views: v.views.map(sub => viewSummary(sub)) }
|
|
99
|
+
: {}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function sessionSummary(session) {
|
|
103
|
+
return {
|
|
104
|
+
name: session.name,
|
|
105
|
+
assemblyNames: session.assemblyNames,
|
|
106
|
+
views: session.views.map(v => viewSummary(v)),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// MST nodes serialize as their snapshot (toJSON), so the live-model walk below
|
|
110
|
+
// can stringify whatever it lands on; the replacer guards what a snapshot
|
|
111
|
+
// cannot contain but a getter's return can.
|
|
112
|
+
export function safeJson(value) {
|
|
113
|
+
// the ANCESTOR chain, not every object seen: MST snapshots are cached and
|
|
114
|
+
// structurally shared, so the same frozen object legitimately appears at two
|
|
115
|
+
// paths, and a visited-set reports the second one as a cycle that is not
|
|
116
|
+
// there. `this` is the holder JSON.stringify is currently walking, which is
|
|
117
|
+
// what lets the chain unwind on the way back up — so this must stay a
|
|
118
|
+
// `function`, not an arrow.
|
|
119
|
+
const chain = [];
|
|
120
|
+
// typed by hand: JSON.stringify's declared return is string, but a bare
|
|
121
|
+
// Symbol/undefined at the top level really does yield undefined at runtime
|
|
122
|
+
const json = JSON.stringify(isStateTreeNode(value) ? getSnapshot(value) : value, function (_key, v) {
|
|
123
|
+
while (chain.length > 0 && chain.at(-1) !== this) {
|
|
124
|
+
chain.pop();
|
|
125
|
+
}
|
|
126
|
+
if (typeof v === 'function') {
|
|
127
|
+
return '[function]';
|
|
128
|
+
}
|
|
129
|
+
if (typeof v === 'bigint') {
|
|
130
|
+
return `${v}`;
|
|
131
|
+
}
|
|
132
|
+
if (v !== null && typeof v === 'object') {
|
|
133
|
+
if (chain.includes(v)) {
|
|
134
|
+
return '[circular]';
|
|
135
|
+
}
|
|
136
|
+
chain.push(v);
|
|
137
|
+
}
|
|
138
|
+
return v;
|
|
139
|
+
});
|
|
140
|
+
return json === undefined ? '"[unserializable]"' : json;
|
|
141
|
+
}
|
|
142
|
+
function describeBrief(value) {
|
|
143
|
+
if (Array.isArray(value)) {
|
|
144
|
+
return `array(${value.length})`;
|
|
145
|
+
}
|
|
146
|
+
if (value !== null && typeof value === 'object') {
|
|
147
|
+
const json = safeJson(value);
|
|
148
|
+
return json.length > 120 ? `object(~${json.length} bytes)` : json;
|
|
149
|
+
}
|
|
150
|
+
const json = safeJson(value);
|
|
151
|
+
return json.length > 120 ? `${json.slice(0, 117)}...` : json;
|
|
152
|
+
}
|
|
153
|
+
// The names a live node answers beyond its snapshot: MST defines views as own
|
|
154
|
+
// getter properties on the instance, which is exactly the high-value surface
|
|
155
|
+
// (visibleLocStrings, assemblyNames, totalBp, ...) a snapshot filters out.
|
|
156
|
+
function memberNames(node) {
|
|
157
|
+
const getters = [];
|
|
158
|
+
const methods = [];
|
|
159
|
+
for (const [key, desc] of Object.entries(Object.getOwnPropertyDescriptors(node))) {
|
|
160
|
+
if (!key.startsWith('$') && key !== 'toJSON') {
|
|
161
|
+
if (desc.get) {
|
|
162
|
+
getters.push(key);
|
|
163
|
+
}
|
|
164
|
+
else if (typeof desc.value === 'function') {
|
|
165
|
+
methods.push(key);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { getters: getters.sort(), methods: methods.sort() };
|
|
170
|
+
}
|
|
171
|
+
function inspectSession(session, args) {
|
|
172
|
+
const path = typeof args.path === 'string' ? args.path : '';
|
|
173
|
+
const maxBytes = typeof args.maxBytes === 'number' ? args.maxBytes : 20_000;
|
|
174
|
+
let node = session;
|
|
175
|
+
const walked = [];
|
|
176
|
+
for (const segment of path.split('.').filter(Boolean)) {
|
|
177
|
+
node =
|
|
178
|
+
node !== null && typeof node === 'object'
|
|
179
|
+
? node[segment]
|
|
180
|
+
: undefined;
|
|
181
|
+
if (node === undefined) {
|
|
182
|
+
throw new Error(`Nothing at "${path}" (undefined after "${walked.join('.') || '(root)'}"). Inspect the parent path to see its keys and getters.`);
|
|
183
|
+
}
|
|
184
|
+
walked.push(segment);
|
|
185
|
+
}
|
|
186
|
+
const members = node !== null && typeof node === 'object' && !Array.isArray(node)
|
|
187
|
+
? memberNames(node)
|
|
188
|
+
: { getters: [], methods: [] };
|
|
189
|
+
const json = safeJson(node);
|
|
190
|
+
const base = {
|
|
191
|
+
path: path || '(session root)',
|
|
192
|
+
// the name the docs tool files a type under: docs topic "model:<modelType>"
|
|
193
|
+
...(isStateTreeNode(node) ? { modelType: getType(node).name } : {}),
|
|
194
|
+
bytes: json.length,
|
|
195
|
+
...(members.getters.length ? { getters: members.getters } : {}),
|
|
196
|
+
...(members.methods.length ? { actions: members.methods } : {}),
|
|
197
|
+
};
|
|
198
|
+
if (json.length <= maxBytes) {
|
|
199
|
+
return { ...base, value: JSON.parse(json) };
|
|
200
|
+
}
|
|
201
|
+
const plain = JSON.parse(json);
|
|
202
|
+
return Array.isArray(plain)
|
|
203
|
+
? {
|
|
204
|
+
...base,
|
|
205
|
+
note: `too large to return whole — ${plain.length} items; index in with .N or raise maxBytes`,
|
|
206
|
+
items: plain.slice(0, 20).map(item => describeBrief(item)),
|
|
207
|
+
}
|
|
208
|
+
: {
|
|
209
|
+
...base,
|
|
210
|
+
note: 'too large to return whole — drill down by path or raise maxBytes',
|
|
211
|
+
keys: Object.fromEntries(Object.entries(plain).map(([k, v]) => [
|
|
212
|
+
k,
|
|
213
|
+
describeBrief(v),
|
|
214
|
+
])),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const delay = (ms) => new Promise(resolve => {
|
|
218
|
+
setTimeout(resolve, ms);
|
|
219
|
+
});
|
|
220
|
+
// The readiness contract is AppReadyMarker's, not this file's: it publishes
|
|
221
|
+
// data-app-phase from the session (ready = no view resolving an assembly, no
|
|
222
|
+
// display fetching), and says so in its own comment.
|
|
223
|
+
//
|
|
224
|
+
// The hold is why the number is a second: `ready` has to survive past the
|
|
225
|
+
// ~600ms FetchVisibleRegions debounce, or one sample taken right after a
|
|
226
|
+
// navigation reads the pre-navigation frame as finished. Same reasoning and
|
|
227
|
+
// same constant as `waitForAppSettled` in products/jbrowse-capture/src/waits.ts
|
|
228
|
+
// (APP_SETTLED_HOLD_MS) — reached independently there, and measured; see
|
|
229
|
+
// REJECTED_IDEAS under "Waiting out a screenshot action's work".
|
|
230
|
+
const READY_HOLD_MS = 1000;
|
|
231
|
+
// An error toast stays up until a human dismisses it, and an info toast leaves
|
|
232
|
+
// on its own — so "the last five messages" repeated one stale error in every
|
|
233
|
+
// settle result for the rest of a run and named no level for the rest. Each
|
|
234
|
+
// toast is delivered to the caller once, by identity: the mobx array keeps the
|
|
235
|
+
// object it was pushed as, and removeSnackbarMessage relies on that too.
|
|
236
|
+
const deliveredNotifications = new WeakMap();
|
|
237
|
+
export function undeliveredNotifications(session) {
|
|
238
|
+
const entries = 'snackbarMessages' in session
|
|
239
|
+
? session.snackbarMessages
|
|
240
|
+
: [];
|
|
241
|
+
const known = deliveredNotifications.get(session);
|
|
242
|
+
const delivered = known ?? new WeakSet();
|
|
243
|
+
if (!known) {
|
|
244
|
+
deliveredNotifications.set(session, delivered);
|
|
245
|
+
}
|
|
246
|
+
const fresh = entries.filter(m => !delivered.has(m));
|
|
247
|
+
for (const m of fresh) {
|
|
248
|
+
delivered.add(m);
|
|
249
|
+
}
|
|
250
|
+
return fresh.map(m => ({
|
|
251
|
+
level: m.level ?? 'info',
|
|
252
|
+
message: m.message,
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
export async function waitReady(timeoutMs, session,
|
|
256
|
+
// scoped rather than document-wide because @jbrowse/react-app2 embeds this
|
|
257
|
+
// app in a host page: two mounted apps publish two ready markers, and a
|
|
258
|
+
// document-wide query answers for whichever is first in document order
|
|
259
|
+
root = document) {
|
|
260
|
+
const deadline = Date.now() + timeoutMs;
|
|
261
|
+
let readySince;
|
|
262
|
+
let outcome;
|
|
263
|
+
while (outcome === undefined) {
|
|
264
|
+
const ready = root.querySelector('[data-app-phase="ready"]') !== null &&
|
|
265
|
+
root.querySelector('[data-testid="loading-overlay"]') === null;
|
|
266
|
+
readySince = ready ? (readySince ?? Date.now()) : undefined;
|
|
267
|
+
if (readySince !== undefined && Date.now() - readySince >= READY_HOLD_MS) {
|
|
268
|
+
outcome = { settled: true };
|
|
269
|
+
}
|
|
270
|
+
else if (Date.now() >= deadline) {
|
|
271
|
+
outcome = {
|
|
272
|
+
settled: false,
|
|
273
|
+
note: 'rendering had not settled by the timeout; tracks may still be loading',
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
await delay(200);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const messages = session ? undeliveredNotifications(session) : [];
|
|
281
|
+
// A display that refuses to draw — over the fetch-size gate, or errored —
|
|
282
|
+
// replaces its own subtree instead of raising a snackbar, so notifications
|
|
283
|
+
// alone report a clean settle over a browser with a blank track in it.
|
|
284
|
+
const notReady = session
|
|
285
|
+
? allTracks(session)
|
|
286
|
+
.map(t => ({ trackId: t.configuration.trackId, ...displayState(t) }))
|
|
287
|
+
.filter(t => 'phase' in t || 'error' in t)
|
|
288
|
+
: [];
|
|
289
|
+
const offscreen = session ? offscreenViews(session, root) : undefined;
|
|
290
|
+
return {
|
|
291
|
+
...outcome,
|
|
292
|
+
...(messages.length ? { notifications: messages } : {}),
|
|
293
|
+
...(notReady.length ? { notReady } : {}),
|
|
294
|
+
...(offscreen ? { offscreen } : {}),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
// A session taller than the window scrolls, and a screenshot of the viewport
|
|
298
|
+
// then shows a plausible browser with the bottom view cut off or the top one
|
|
299
|
+
// scrolled away: every filmed take spent turns shrinking tracks after a
|
|
300
|
+
// picture, and one found its genome view at y = -267. Reported from the DOM
|
|
301
|
+
// the views are laid out in, so the agent knows before it looks.
|
|
302
|
+
function offscreenViews(session, root) {
|
|
303
|
+
const win = root.ownerDocument?.defaultView ?? window;
|
|
304
|
+
const windowHeight = win.innerHeight;
|
|
305
|
+
const pageHeight = win.document.documentElement.scrollHeight;
|
|
306
|
+
const containers = new Map([
|
|
307
|
+
...root.querySelectorAll('[data-testid^="view-container-"]'),
|
|
308
|
+
].map(el => [el.dataset.testid.slice('view-container-'.length), el]));
|
|
309
|
+
const views = allViews(session).flatMap(view => {
|
|
310
|
+
const rect = containers.get(view.id)?.getBoundingClientRect();
|
|
311
|
+
return rect && (rect.top < 0 || rect.bottom > windowHeight)
|
|
312
|
+
? [
|
|
313
|
+
{
|
|
314
|
+
viewId: view.id,
|
|
315
|
+
top: Math.round(rect.top),
|
|
316
|
+
bottom: Math.round(rect.bottom),
|
|
317
|
+
},
|
|
318
|
+
]
|
|
319
|
+
: [];
|
|
320
|
+
});
|
|
321
|
+
return pageHeight > windowHeight || views.length
|
|
322
|
+
? {
|
|
323
|
+
pageHeight,
|
|
324
|
+
windowHeight,
|
|
325
|
+
views,
|
|
326
|
+
note: 'the session is taller than the window; a viewport screenshot cuts these views off — shrink track heights, or screenshot with fullPage: true',
|
|
327
|
+
}
|
|
328
|
+
: undefined;
|
|
329
|
+
}
|
|
330
|
+
function trackEntry(conf) {
|
|
331
|
+
const adapter = readConfObject(conf, 'adapter');
|
|
332
|
+
return {
|
|
333
|
+
trackId: conf.trackId,
|
|
334
|
+
name: readConfObject(conf, 'name'),
|
|
335
|
+
type: conf.type,
|
|
336
|
+
...(adapter.type ? { adapterType: adapter.type } : {}),
|
|
337
|
+
assemblyNames: readConfObject(conf, 'assemblyNames'),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function listTracks(session, searchArg, limitArg) {
|
|
341
|
+
const search = searchArg?.toLowerCase() ?? '';
|
|
342
|
+
const limit = limitArg ?? 100;
|
|
343
|
+
// allSessionTracks, not session.tracks: connection-supplied tracks (hubs,
|
|
344
|
+
// registries) are absent from the session lists but fully showable — a
|
|
345
|
+
// hand-rolled union here hid them from agents entirely
|
|
346
|
+
const matches = allSessionTracks(session)
|
|
347
|
+
.map(c => trackEntry(c))
|
|
348
|
+
.filter(t => !search ||
|
|
349
|
+
t.trackId.toLowerCase().includes(search) ||
|
|
350
|
+
t.name.toLowerCase().includes(search));
|
|
351
|
+
return {
|
|
352
|
+
total: matches.length,
|
|
353
|
+
...(matches.length > limit
|
|
354
|
+
? { note: `showing ${limit} of ${matches.length}; refine with search` }
|
|
355
|
+
: {}),
|
|
356
|
+
tracks: matches.slice(0, limit),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function pickView(session, args, capability, wants) {
|
|
360
|
+
const viewId = typeof args.viewId === 'string' ? args.viewId : '';
|
|
361
|
+
const candidates = allViews(session);
|
|
362
|
+
if (viewId) {
|
|
363
|
+
const named = candidates.find(v => v.id === viewId);
|
|
364
|
+
if (!named) {
|
|
365
|
+
throw new Error(`No view with id "${viewId}". Open views: ${candidates.map(v => `${v.id} (${v.type})`).join(', ')}`);
|
|
366
|
+
}
|
|
367
|
+
if (typeof viewSelf(named)[capability] !== 'function') {
|
|
368
|
+
throw new Error(`View ${named.id} (${named.type}) does not support this`);
|
|
369
|
+
}
|
|
370
|
+
return named;
|
|
371
|
+
}
|
|
372
|
+
const able = candidates.filter(v => typeof viewSelf(v)[capability] === 'function');
|
|
373
|
+
if (!able.length) {
|
|
374
|
+
throw new Error(`No open view supports this. Open views: ${candidates.map(v => v.type).join(', ') || 'none'} — jb.loadSessionSpec can open one.`);
|
|
375
|
+
}
|
|
376
|
+
// A view on another assembly would show the track and render nothing, with a
|
|
377
|
+
// successful-looking result — the same silent mismatch visibleRegionsOf
|
|
378
|
+
// guards against for reads
|
|
379
|
+
const onAssembly = wants?.assembly
|
|
380
|
+
? able.filter(v => viewSelf(v).assemblyNames?.some(name => isSameAssemblyName(name, wants.assembly, session.assemblyManager)))
|
|
381
|
+
: able;
|
|
382
|
+
if (!onAssembly.length) {
|
|
383
|
+
throw new Error(`No open view is on assembly "${wants.assembly}" (open views: ${able.map(v => `${v.id} on ${viewSelf(v).assemblyNames?.join(', ')}`).join('; ')}) — open one, or pass show: false`);
|
|
384
|
+
}
|
|
385
|
+
const canDisplay = wants?.trackType && wants.pluginManager
|
|
386
|
+
? onAssembly.filter(v => viewCanDisplayTrack(wants.pluginManager, viewDisplayNames(wants.pluginManager, v.type), wants.trackType))
|
|
387
|
+
: onAssembly;
|
|
388
|
+
if (!canDisplay.length) {
|
|
389
|
+
throw new Error(`No open view can display a ${wants.trackType} (open views: ${onAssembly.map(v => v.type).join(', ')})`);
|
|
390
|
+
}
|
|
391
|
+
return canDisplay[0];
|
|
392
|
+
}
|
|
393
|
+
function shownTrackModel(session, trackId) {
|
|
394
|
+
return allTracks(session).find(t => t.configuration.trackId === trackId);
|
|
395
|
+
}
|
|
396
|
+
async function locToRegion(session, conf, loc, assemblyArg) {
|
|
397
|
+
// getConfAssemblyNamesOrNone, not the assemblyNames slot: an assembly's own
|
|
398
|
+
// sequence track has no such slot and answers through its parent assembly
|
|
399
|
+
const assemblyName = assemblyArg ?? getConfAssemblyNamesOrNone(conf)[0];
|
|
400
|
+
if (assemblyName === undefined) {
|
|
401
|
+
throw new Error('The track names no assembly; pass assembly explicitly');
|
|
402
|
+
}
|
|
403
|
+
const assembly = await session.assemblyManager.waitForAssembly(assemblyName);
|
|
404
|
+
if (!assembly) {
|
|
405
|
+
throw new Error(`Assembly "${assemblyName}" could not be loaded`);
|
|
406
|
+
}
|
|
407
|
+
const parsed = parseLocString(loc, refName => assembly.isValidRefName(refName));
|
|
408
|
+
const refName = assembly.getCanonicalRefName(parsed.refName) ?? parsed.refName;
|
|
409
|
+
const bounds = assembly.regions?.find(r => r.refName === refName);
|
|
410
|
+
return {
|
|
411
|
+
assemblyName,
|
|
412
|
+
refName,
|
|
413
|
+
start: parsed.start ?? bounds?.start ?? 0,
|
|
414
|
+
end: parsed.end ?? bounds?.end ?? Number.MAX_SAFE_INTEGER,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
async function visibleRegionsOf(session, viewId, preferTrackId) {
|
|
418
|
+
// `in`, not evaluation: visibleRegions is a getter that THROWS ("width
|
|
419
|
+
// undefined") until the view's component mounts and sets a width — a
|
|
420
|
+
// freshly spec-loaded view stays in that state briefly even after the
|
|
421
|
+
// app-phase marker reads ready, since a view with no width has no display
|
|
422
|
+
// fetching anything.
|
|
423
|
+
const candidates = allViews(session).filter(v => (!viewId || v.id === viewId) && 'visibleRegions' in v);
|
|
424
|
+
// among region-bearing views, the one actually showing the track wins — two
|
|
425
|
+
// views on two assemblies would otherwise send the first view's namespace to
|
|
426
|
+
// the second view's file, which answers nothing, silently
|
|
427
|
+
const chosen = candidates.find(v => viewTracks(v).some(t => t.configuration.trackId === preferTrackId)) ?? candidates[0];
|
|
428
|
+
if (!chosen) {
|
|
429
|
+
throw new Error('No view that shows a region — pass loc, or open a linear view first');
|
|
430
|
+
}
|
|
431
|
+
const view = viewSelf(chosen);
|
|
432
|
+
const deadline = Date.now() + 10_000;
|
|
433
|
+
let visible;
|
|
434
|
+
while (visible === undefined) {
|
|
435
|
+
if (view.initialized !== false) {
|
|
436
|
+
try {
|
|
437
|
+
visible = view.visibleRegions;
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
// not mounted yet
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (visible?.length) {
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
visible = undefined;
|
|
447
|
+
if (Date.now() >= deadline) {
|
|
448
|
+
throw new Error('The view has not finished initializing a visible region — pass loc, or navigate first');
|
|
449
|
+
}
|
|
450
|
+
await delay(250);
|
|
451
|
+
}
|
|
452
|
+
// whole bases: a block edge is a fractional bp, and 30000.000000000004
|
|
453
|
+
// reads as a bug in every answer that echoes it
|
|
454
|
+
return visible.map(({ refName, start, end, assemblyName }) => ({
|
|
455
|
+
refName,
|
|
456
|
+
start: Math.floor(start),
|
|
457
|
+
end: Math.ceil(end),
|
|
458
|
+
assemblyName,
|
|
459
|
+
}));
|
|
460
|
+
}
|
|
461
|
+
// Main-thread adapter, not the CoreGetFeatures RPC: the RPC serializes every
|
|
462
|
+
// feature in the region across the worker boundary before any limit can
|
|
463
|
+
// apply. Here the features stay objects. The shown track's own rpcSessionId,
|
|
464
|
+
// so this shares the adapter instance — parsed indexes and chunk caches
|
|
465
|
+
// included — that the display already warmed (rpcSessionId lives on track
|
|
466
|
+
// models, so the walk cannot start at the session; session.id is the
|
|
467
|
+
// cold-namespace fallback for un-shown tracks). Regions are renamed the same
|
|
468
|
+
// way the RPC base class renames them: they arrive carrying the assembly's
|
|
469
|
+
// canonical refNames, and a file spelling them differently would otherwise
|
|
470
|
+
// answer nothing, silently.
|
|
471
|
+
async function fetchFeatures(pluginManager, session, trackId, regions, requestedByteLimit) {
|
|
472
|
+
const conf = session.getTrackById(trackId);
|
|
473
|
+
if (!conf) {
|
|
474
|
+
throw new Error(`No track with trackId "${trackId}" — jb.listTracks() shows what is available`);
|
|
475
|
+
}
|
|
476
|
+
const trackModel = shownTrackModel(session, trackId);
|
|
477
|
+
const sessionId = trackModel
|
|
478
|
+
? getRpcSessionId(trackModel)
|
|
479
|
+
: (session.id ?? 'mcp');
|
|
480
|
+
const renamed = await renameRegionsIfNeeded(session.assemblyManager, {
|
|
481
|
+
regions,
|
|
482
|
+
adapterConfig: readConfObject(conf, 'adapter'),
|
|
483
|
+
sessionId,
|
|
484
|
+
});
|
|
485
|
+
const dataAdapter = await getFeatureAdapterOrThrow({
|
|
486
|
+
pluginManager,
|
|
487
|
+
sessionId,
|
|
488
|
+
adapterConfig: renamed.adapterConfig,
|
|
489
|
+
sequenceAdapter: renamed.sequenceAdapter,
|
|
490
|
+
});
|
|
491
|
+
// The adapter's own declared limit where it has one, so this does not quietly
|
|
492
|
+
// disagree with the size the track's display already refuses to render —
|
|
493
|
+
// the reasoning BaseTrackModel.exportByteLimit spells out for "Save track
|
|
494
|
+
// data", which is the other path that fetches because someone asked rather
|
|
495
|
+
// than because a view scrolled.
|
|
496
|
+
const byteLimit = agentByteLimit(readConfObject(conf, ['adapter', 'fetchSizeLimit']), requestedByteLimit);
|
|
497
|
+
const stopToken = createStopToken();
|
|
498
|
+
// desktop's MCP relay gives up at 150s, so the read must not outlive it —
|
|
499
|
+
// and an agent-triggered read of a dense region wants a ceiling either way
|
|
500
|
+
const stopTimer = setTimeout(() => {
|
|
501
|
+
stopStopToken(stopToken);
|
|
502
|
+
}, 120_000);
|
|
503
|
+
// The agent's "region too large". A display refuses to fetch over its own
|
|
504
|
+
// gate and paints the reason; this path has no display, so without the same
|
|
505
|
+
// question asked here an agent that names a whole chromosome pulls it — over
|
|
506
|
+
// someone else's data host, onto the main thread, in the page it is about to
|
|
507
|
+
// screenshot. Same index-only estimate the gated RPC takes first, and a
|
|
508
|
+
// refusal rather than a truncation: a short answer that looked like the whole
|
|
509
|
+
// answer is the failure this surface exists to prevent.
|
|
510
|
+
const { tooLarge, bytes } = await measureRegionBytes({
|
|
511
|
+
dataAdapter,
|
|
512
|
+
regions: renamed.regions,
|
|
513
|
+
byteLimit,
|
|
514
|
+
stopToken,
|
|
515
|
+
});
|
|
516
|
+
if (tooLarge) {
|
|
517
|
+
clearTimeout(stopTimer);
|
|
518
|
+
throw new Error(`region too large for jb.getFeatures: the largest region is ~${bytes} bytes against a limit of ${byteLimit}. Narrow the region, or pass an explicit byteLimit if you mean to pull this much.`);
|
|
519
|
+
}
|
|
520
|
+
// one array per region, flattened once at the end: a dense region returns
|
|
521
|
+
// hundreds of thousands of features, so neither push(...array) — a
|
|
522
|
+
// RangeError on the argument list — nor a concat per region is available
|
|
523
|
+
const perRegion = [];
|
|
524
|
+
try {
|
|
525
|
+
for (const region of renamed.regions) {
|
|
526
|
+
perRegion.push(await dataAdapter.getFeaturesArray(region, { stopToken }));
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
finally {
|
|
530
|
+
clearTimeout(stopTimer);
|
|
531
|
+
}
|
|
532
|
+
return perRegion.flat();
|
|
533
|
+
}
|
|
534
|
+
// A LocalPathLocation only reads under Electron: in a browser openLocation
|
|
535
|
+
// throws "can't use local files in the browser" at the FIRST READ, not here, so
|
|
536
|
+
// accepting one would report a track added and then fail inside the display
|
|
537
|
+
// where the agent is least likely to connect it to what it asked for.
|
|
538
|
+
function fileLocation(spec) {
|
|
539
|
+
if (/^https?:\/\//.test(spec)) {
|
|
540
|
+
return { uri: spec, locationType: 'UriLocation' };
|
|
541
|
+
}
|
|
542
|
+
if (!isElectron) {
|
|
543
|
+
throw new Error(`jb.addTrack needs a URL in a browser, and "${spec}" is a local path. JBrowse Web cannot read local files except through the Add track file picker, which needs a real file chosen by hand.`);
|
|
544
|
+
}
|
|
545
|
+
return { localPath: spec, locationType: 'LocalPathLocation' };
|
|
546
|
+
}
|
|
547
|
+
// A stacked set of bigWigs is one track over a MultiWiggleAdapter, which the
|
|
548
|
+
// per-file guesser cannot express — the web take hand-wrote the config. The
|
|
549
|
+
// subadapters form rather than the `bigWigs` shorthand: that one takes
|
|
550
|
+
// absolute URLs only, and desktop's locations are paths.
|
|
551
|
+
// the file's name without its bigWig extension, or undefined for any other file
|
|
552
|
+
function bigWigStem(location) {
|
|
553
|
+
const fileName = location.split(/[/\\]/).pop() ?? location;
|
|
554
|
+
const match = /^(.+)\.(bw|bigwig)$/i.exec(fileName);
|
|
555
|
+
return match?.[1];
|
|
556
|
+
}
|
|
557
|
+
function multiWiggleTrackConf(locations, assemblyName) {
|
|
558
|
+
const stems = locations.map(l => bigWigStem(l));
|
|
559
|
+
const notBigWig = locations.filter((_, i) => stems[i] === undefined);
|
|
560
|
+
if (notBigWig.length) {
|
|
561
|
+
throw new Error(`jb.addTrack takes a list of locations only for bigWigs (one stacked MultiQuantitativeTrack); not bigWig: ${notBigWig.join(', ')}. Add other formats one at a time.`);
|
|
562
|
+
}
|
|
563
|
+
const adapter = {
|
|
564
|
+
type: 'MultiWiggleAdapter',
|
|
565
|
+
subadapters: locations.map((l, i) => ({
|
|
566
|
+
type: 'BigWigAdapter',
|
|
567
|
+
name: stems[i],
|
|
568
|
+
bigWigLocation: fileLocation(l),
|
|
569
|
+
})),
|
|
570
|
+
};
|
|
571
|
+
return {
|
|
572
|
+
trackId: `multiwiggle-${objectHash(adapter).slice(0, 8)}`,
|
|
573
|
+
type: 'MultiQuantitativeTrack',
|
|
574
|
+
name: stems.join(', '),
|
|
575
|
+
assemblyNames: [assemblyName],
|
|
576
|
+
adapter,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
// one location, a list of them, or nothing usable
|
|
580
|
+
function locationsOf(value) {
|
|
581
|
+
return Array.isArray(value)
|
|
582
|
+
? value.filter((l) => typeof l === 'string')
|
|
583
|
+
: typeof value === 'string'
|
|
584
|
+
? value
|
|
585
|
+
: '';
|
|
586
|
+
}
|
|
587
|
+
// Vocabulary introspection: every config slot a live config node's schema
|
|
588
|
+
// defines, so code never has to guess which settings keys exist — an unknown
|
|
589
|
+
// key is otherwise dropped silently, which is this format's known failure mode.
|
|
590
|
+
function describeSlots(conf) {
|
|
591
|
+
const definition = getConfigurationSchemaDefinition(conf) ?? {};
|
|
592
|
+
return Object.fromEntries(Object.entries(definition).flatMap(([name, def]) => isSlotDefinitionEntry(def)
|
|
593
|
+
? [
|
|
594
|
+
[
|
|
595
|
+
name,
|
|
596
|
+
{
|
|
597
|
+
type: def.type,
|
|
598
|
+
...(def.description ? { description: def.description } : {}),
|
|
599
|
+
defaultValue: def.defaultValue,
|
|
600
|
+
},
|
|
601
|
+
],
|
|
602
|
+
]
|
|
603
|
+
: []));
|
|
604
|
+
}
|
|
605
|
+
// The raw primitive under all of the above: Claude-authored code against the
|
|
606
|
+
// live model graph. The renderer already runs with nodeIntegration and the
|
|
607
|
+
// bridge socket is user-only, so this grants what the surface as a whole
|
|
608
|
+
// already grants — expressed directly instead of through a curated verb.
|
|
609
|
+
// The re-export registry is what pluginManager.jbrequire serves: the same
|
|
610
|
+
// pinned ABI module names external plugins link against (ReExports/modules.ts,
|
|
611
|
+
// abiBaseline.json). It stays empty until a runtime plugin loads, so each
|
|
612
|
+
// evaluate (re)installs it — import() is memoized, so this is one lookup and
|
|
613
|
+
// an idempotent assignment, with no module-level flag to hold.
|
|
614
|
+
export async function ensureReExports() {
|
|
615
|
+
setReExportRegistry((await import('@jbrowse/core/ReExports/modules')).default);
|
|
616
|
+
}
|
|
617
|
+
// The helper library an agent drives the app through. Built from the plugin
|
|
618
|
+
// manager alone, so one of these serves a whole app rather than one session —
|
|
619
|
+
// which is what lets jbrowse-web hand the same object to every caller for the
|
|
620
|
+
// life of a plugin manager.
|
|
621
|
+
export function createJbApi(pluginManager) {
|
|
622
|
+
// Resolved per call, never captured: jb.loadSessionSpec REPLACES the session,
|
|
623
|
+
// and a helper bound to the old one keeps answering from a detached tree —
|
|
624
|
+
// which reads as stale data rather than throwing, so the agent is told about
|
|
625
|
+
// a session that no longer exists. `jb.session` is how code re-reads it after
|
|
626
|
+
// a spec load.
|
|
627
|
+
const live = () => {
|
|
628
|
+
const current = sessionOf(pluginManager);
|
|
629
|
+
if (!current) {
|
|
630
|
+
throw new Error('No session is open');
|
|
631
|
+
}
|
|
632
|
+
return current;
|
|
633
|
+
};
|
|
634
|
+
const jb = {
|
|
635
|
+
get session() {
|
|
636
|
+
return live();
|
|
637
|
+
},
|
|
638
|
+
get rootModel() {
|
|
639
|
+
return pluginManager.rootModel;
|
|
640
|
+
},
|
|
641
|
+
require: pluginManager.jbrequire,
|
|
642
|
+
ensureRequire: ensureReExports,
|
|
643
|
+
mst,
|
|
644
|
+
mobx,
|
|
645
|
+
readConfObject,
|
|
646
|
+
getConf,
|
|
647
|
+
describeSlots,
|
|
648
|
+
parseLocString,
|
|
649
|
+
getFeatureAdapterOrThrow,
|
|
650
|
+
getRpcSessionId,
|
|
651
|
+
renameRegionsIfNeeded,
|
|
652
|
+
createStopToken,
|
|
653
|
+
stopStopToken,
|
|
654
|
+
waitReady: (timeoutMs) => waitReady(timeoutMs, live()),
|
|
655
|
+
sessionSummary: () => sessionSummary(live()),
|
|
656
|
+
inspect: (path, maxInspectBytes) => inspectSession(live(), { path, maxBytes: maxInspectBytes }),
|
|
657
|
+
listTracks: (search, limit) => listTracks(live(), search, limit),
|
|
658
|
+
trackModel: (trackId) => shownTrackModel(live(), trackId),
|
|
659
|
+
visibleRegions: (viewId) => visibleRegionsOf(live(), viewId),
|
|
660
|
+
loadSessionSpec: (spec, settleMs) => loadSpec(pluginManager, { spec, settleMs }),
|
|
661
|
+
addTrack: (opts) => addTrack(pluginManager, live(), opts),
|
|
662
|
+
// Two of four filmed takes wrote jb.getFeatures('trackId', loc) and lost a
|
|
663
|
+
// turn to "No track with trackId undefined" — so the positional form is
|
|
664
|
+
// simply accepted alongside the object it was documented as.
|
|
665
|
+
getFeatures: async (args, positionalLoc) => {
|
|
666
|
+
const fetchArgs = typeof args === 'string' ? { trackId: args, loc: positionalLoc } : args;
|
|
667
|
+
const session = live();
|
|
668
|
+
const conf = session.getTrackById(fetchArgs.trackId);
|
|
669
|
+
if (!conf) {
|
|
670
|
+
throw new Error(`No track with trackId "${fetchArgs.trackId}" — jb.listTracks() shows what is available`);
|
|
671
|
+
}
|
|
672
|
+
const regions = fetchArgs.regions ??
|
|
673
|
+
(fetchArgs.loc !== undefined
|
|
674
|
+
? [
|
|
675
|
+
await locToRegion(session, conf, fetchArgs.loc, fetchArgs.assembly),
|
|
676
|
+
]
|
|
677
|
+
: await visibleRegionsOf(session, fetchArgs.viewId, fetchArgs.trackId));
|
|
678
|
+
return fetchFeatures(pluginManager, session, fetchArgs.trackId, regions, fetchArgs.byteLimit);
|
|
679
|
+
},
|
|
680
|
+
};
|
|
681
|
+
return jb;
|
|
682
|
+
}
|
|
683
|
+
async function addTrack(pluginManager, session, args) {
|
|
684
|
+
if (!isSessionWithAddSessionTrack(session)) {
|
|
685
|
+
throw new Error('This session cannot add tracks');
|
|
686
|
+
}
|
|
687
|
+
const location = locationsOf(args.location);
|
|
688
|
+
if (!location.length) {
|
|
689
|
+
throw new Error('jb.addTrack needs a location (local path or URL)');
|
|
690
|
+
}
|
|
691
|
+
const requested = typeof args.assembly === 'string' ? args.assembly : session.assemblyNames[0];
|
|
692
|
+
if (requested === undefined) {
|
|
693
|
+
throw new Error('The session has no assemblies to attach the track to');
|
|
694
|
+
}
|
|
695
|
+
// resolved to the session's own spelling: an alias ("vvx" for "volvox",
|
|
696
|
+
// "GRCh38" for "hg38") names the assembly and would otherwise be refused
|
|
697
|
+
const assembly = session.assemblyNames.find(name => isSameAssemblyName(name, requested, session.assemblyManager));
|
|
698
|
+
if (assembly === undefined) {
|
|
699
|
+
throw new Error(`Assembly "${requested}" is not in this session (has: ${session.assemblyNames.join(', ')})`);
|
|
700
|
+
}
|
|
701
|
+
const conf = Array.isArray(location)
|
|
702
|
+
? multiWiggleTrackConf(location, assembly)
|
|
703
|
+
: guessTrackConfForLocation(fileLocation(location), typeof args.index === 'string' ? fileLocation(args.index) : undefined, pluginManager, assembly);
|
|
704
|
+
session.addSessionTrackConf({
|
|
705
|
+
...conf,
|
|
706
|
+
...(typeof args.name === 'string' ? { name: args.name } : {}),
|
|
707
|
+
});
|
|
708
|
+
const summary = {
|
|
709
|
+
trackId: conf.trackId,
|
|
710
|
+
trackType: conf.type,
|
|
711
|
+
adapterType: conf.adapter.type,
|
|
712
|
+
assembly,
|
|
713
|
+
};
|
|
714
|
+
if (args.show === false) {
|
|
715
|
+
return summary;
|
|
716
|
+
}
|
|
717
|
+
const view = pickView(session, args, 'showTrack', {
|
|
718
|
+
assembly,
|
|
719
|
+
trackType: conf.type,
|
|
720
|
+
pluginManager,
|
|
721
|
+
});
|
|
722
|
+
viewSelf(view).showTrack(conf.trackId);
|
|
723
|
+
// a spec load on a busy machine passed 30s and answered settled:false over
|
|
724
|
+
// views that were fine; the wait exits the moment they are ready
|
|
725
|
+
const settle = await waitReady(60_000, session);
|
|
726
|
+
return { ...summary, ...settle, shownInView: view.id };
|
|
727
|
+
}
|
|
728
|
+
// Under the 45 s one evaluation gets from the Claude in Chrome extension: a
|
|
729
|
+
// cold hosted config settling for the full minute answered nothing there,
|
|
730
|
+
// while the settle result already names what is still not ready
|
|
731
|
+
const SPEC_SETTLE_DEFAULT_MS = 30_000;
|
|
732
|
+
async function loadSpec(pluginManager, args) {
|
|
733
|
+
const spec = args.spec;
|
|
734
|
+
const valid = typeof spec === 'object' &&
|
|
735
|
+
spec !== null &&
|
|
736
|
+
Array.isArray(spec.views);
|
|
737
|
+
if (!valid) {
|
|
738
|
+
throw new Error('loadSessionSpec needs a spec object with a views array');
|
|
739
|
+
}
|
|
740
|
+
await loadSessionSpec(spec, pluginManager);
|
|
741
|
+
// the session was REPLACED by the spec load — settle against the new one
|
|
742
|
+
const session = sessionOf(pluginManager);
|
|
743
|
+
const settle = await waitReady(args.settleMs ?? SPEC_SETTLE_DEFAULT_MS, session);
|
|
744
|
+
return {
|
|
745
|
+
...settle,
|
|
746
|
+
...(session ? { session: sessionSummary(session) } : {}),
|
|
747
|
+
};
|
|
748
|
+
}
|