@jbrowse/app-core 4.3.0 → 5.0.0-beta.10
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 +1324 -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 +294 -0
- package/esm/Assemblies/AssembliesMixin.js +154 -0
- package/esm/Assemblies/index.d.ts +1 -2
- package/esm/Assemblies/index.js +1 -2
- package/esm/HistoryManagement/index.d.ts +11 -0
- package/esm/HistoryManagement/index.js +43 -10
- package/esm/JBrowseConfig/RootConfiguration.d.ts +123 -78
- package/esm/JBrowseConfig/RootConfiguration.js +79 -7
- package/esm/JBrowseConfig/defaultShareUrl.d.ts +1 -0
- package/esm/JBrowseConfig/defaultShareUrl.js +5 -0
- package/esm/JBrowseConfig/index.d.ts +175 -84
- package/esm/JBrowseConfig/index.js +64 -2
- package/esm/JBrowseModel/index.d.ts +175 -97
- package/esm/JBrowseModel/index.js +132 -31
- package/esm/JbApi/index.d.ts +2 -0
- package/esm/JbApi/index.js +1 -0
- package/esm/JbApi/jbApi.d.ts +2767 -0
- package/esm/JbApi/jbApi.js +1383 -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 +161 -0
- package/esm/SessionSpec/loadSessionSpec.d.ts +75 -0
- package/esm/SessionSpec/loadSessionSpec.js +473 -0
- package/esm/SessionSpec/parseSessionSpecUrl.d.ts +7 -0
- package/esm/SessionSpec/parseSessionSpecUrl.js +181 -0
- package/esm/SessionSpec/types.d.ts +37 -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 +107 -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 +105 -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 +260 -0
- package/esm/WorkspaceLayout/model.js +586 -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 +93 -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 +39 -19
- package/esm/ui/App/AppFab.d.ts +12 -3
- package/esm/ui/App/AppFab.js +15 -8
- package/esm/ui/App/AppToolbar.d.ts +2 -14
- package/esm/ui/App/AppToolbar.js +45 -31
- package/esm/ui/App/ClassicViewsContainer.d.ts +4 -10
- 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/DrawerHeader.d.ts +2 -3
- package/esm/ui/App/DrawerHeader.js +24 -11
- 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/ThemeModeButton.d.ts +12 -0
- package/esm/ui/App/ThemeModeButton.js +34 -0
- 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 +128 -24
- package/esm/ui/App/ViewContainerTitle.d.ts +1 -1
- package/esm/ui/App/ViewContainerTitle.js +21 -11
- 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 +48 -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 +4 -11
- package/esm/ui/App/ViewsContainer.js +6 -9
- 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/lazyParts.d.ts +32 -0
- package/esm/ui/App/lazyParts.js +8 -0
- package/esm/ui/App/preloadAppShell.d.ts +14 -0
- package/esm/ui/App/preloadAppShell.js +41 -0
- package/esm/ui/App/types.d.ts +17 -3
- package/esm/ui/App/types.js +1 -1
- package/esm/ui/App/useViewVisibility.d.ts +35 -0
- package/esm/ui/App/useViewVisibility.js +88 -0
- package/esm/ui/App/viewTitle.d.ts +12 -0
- package/esm/ui/App/viewTitle.js +17 -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/DrawerControls.d.ts +0 -5
- package/esm/ui/App/DrawerControls.js +0 -25
- 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/copyView.d.ts +0 -1
- package/esm/ui/App/copyView.js +0 -29
- package/esm/ui/App/dockviewUtils.d.ts +0 -46
- package/esm/ui/App/dockviewUtils.js +0 -55
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { isSessionWithAddAssembly, isSessionWithAddSessionTrack, } from '@jbrowse/core/util';
|
|
2
|
+
import { legacyInitMessage, unknownKeysMessage, } from '@jbrowse/core/util/withLaunchInput';
|
|
3
|
+
import { isAlive, isStateTreeNode } from '@jbrowse/mobx-state-tree';
|
|
4
|
+
import { when } from 'mobx';
|
|
5
|
+
// A spec `layout` needs `layoutViews`, which a session composing
|
|
6
|
+
// WorkspaceLayoutMixin and MultipleViewsSessionMixin has and an embedded
|
|
7
|
+
// product's does not. It is looked up behind an `in` guard, so renaming it
|
|
8
|
+
// without renaming it here makes every spec layout silently decline rather than
|
|
9
|
+
// fail to compile; see app-core/src/WorkspaceLayout/CLAUDE.md.
|
|
10
|
+
function hasLayoutViews(session) {
|
|
11
|
+
return 'layoutViews' in session;
|
|
12
|
+
}
|
|
13
|
+
function isSessionWithSpecConnections(session) {
|
|
14
|
+
return ('addConnectionConf' in session &&
|
|
15
|
+
'makeConnection' in session &&
|
|
16
|
+
'connectionInstances' in session);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Register `&sessionTracks=` / a spec's `sessionTracks` into a session.
|
|
20
|
+
*
|
|
21
|
+
* Exported because the hub launch needs it too and must not reimplement it.
|
|
22
|
+
*
|
|
23
|
+
* A track that fails to register does not cost the caller the rest of them —
|
|
24
|
+
* same reasoning as the per-connection and per-view try/catch around it, one
|
|
25
|
+
* layer down.
|
|
26
|
+
*/
|
|
27
|
+
export function addSessionTracks(session, tracks) {
|
|
28
|
+
if (!tracks.length) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (!isSessionWithAddSessionTrack(session)) {
|
|
32
|
+
session?.notifyError('This link has "sessionTracks", but this application cannot add tracks to a session');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
for (const track of tracks) {
|
|
36
|
+
const label = typeof track.trackId === 'string' ? track.trackId : '?';
|
|
37
|
+
try {
|
|
38
|
+
session.addSessionTrackConf(track);
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
console.error(e);
|
|
42
|
+
session.notifyError(`Track "${label}" has an invalid configuration: ${e}`, e);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// A connection supplies assemblies and tracks that the spec's views go on to
|
|
47
|
+
// reference by name, and it supplies them from a fetch. Launching a view before
|
|
48
|
+
// that fetch lands gives it an assembly that doesn't exist yet ("Assembly X not
|
|
49
|
+
// found") and a `tracks` list whose ids don't resolve yet.
|
|
50
|
+
//
|
|
51
|
+
// Causal, not timed: every connection reports `loading` from its own
|
|
52
|
+
// afterAttach until its connect() promise settles, success or failure, so this
|
|
53
|
+
// proceeds the moment the last one is done and never waits on one that failed.
|
|
54
|
+
// A session destroyed mid-wait (a StrictMode remount, a session swap) resolves
|
|
55
|
+
// it too — reading a detached node throws, and here that would land in the
|
|
56
|
+
// caller's catch and report a spurious error against the *new* session.
|
|
57
|
+
async function whenConnectionsSettle(session) {
|
|
58
|
+
// isStateTreeNode first: the liveness question only applies to a real MST
|
|
59
|
+
// session, and this is called with plain object sessions under test
|
|
60
|
+
const gone = () => isStateTreeNode(session) && !isAlive(session);
|
|
61
|
+
await when(() => gone() || session.connectionInstances.every(conn => !conn.loading));
|
|
62
|
+
return !gone();
|
|
63
|
+
}
|
|
64
|
+
// Resolve a spec layout's view indexes to ids before `applyLayoutSpec` counts
|
|
65
|
+
// them into `session.views`, which is a different list: a spec index names a
|
|
66
|
+
// spec entry, and `createdViewIds[i]` is EVERY view the i-th entry created, in
|
|
67
|
+
// creation order. A connected ProteinView creates its genome view and then
|
|
68
|
+
// itself, and the index names both — stacked in one cell — where recording one
|
|
69
|
+
// id per entry left the structure unaddressable, in no cell, and the panel
|
|
70
|
+
// naming it empty. A string is a view id and passes through; a reference that
|
|
71
|
+
// resolves to nothing is dropped here after `unresolvedLayoutRefs` reported it.
|
|
72
|
+
function convertLayoutNode(node, createdViewIds, knownIds, pinnedIds) {
|
|
73
|
+
const { views, children, ...rest } = node;
|
|
74
|
+
return {
|
|
75
|
+
...rest,
|
|
76
|
+
...(views === undefined
|
|
77
|
+
? {}
|
|
78
|
+
: {
|
|
79
|
+
views: Array.isArray(views)
|
|
80
|
+
? views.flatMap(ref => typeof ref === 'number'
|
|
81
|
+
? (createdViewIds[ref] ?? []).filter(id => !pinnedIds.has(id))
|
|
82
|
+
: knownIds.has(ref)
|
|
83
|
+
? [ref]
|
|
84
|
+
: [])
|
|
85
|
+
: views,
|
|
86
|
+
}),
|
|
87
|
+
...(children === undefined
|
|
88
|
+
? {}
|
|
89
|
+
: {
|
|
90
|
+
children: Array.isArray(children)
|
|
91
|
+
? children.map(child => convertLayoutNode(child, createdViewIds, knownIds, pinnedIds))
|
|
92
|
+
: children,
|
|
93
|
+
}),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
// An index names every view its entry created, so the entry that opens a genome
|
|
97
|
+
// view beside its own puts BOTH in the cell naming it — which is the point, and
|
|
98
|
+
// is wrong the moment another cell names one of them by id. `{views:[0]}` next
|
|
99
|
+
// to `{views:['structure']}` is the obvious spelling of "genome left, structure
|
|
100
|
+
// right", and expanding the index blind seats the structure in both cells: two
|
|
101
|
+
// React trees and two GPU contexts for one model, which `resolveLayoutSpec` now
|
|
102
|
+
// refuses outright. An id stated by hand is the more specific statement, so it
|
|
103
|
+
// wins its view and the index gives it up.
|
|
104
|
+
function pinnedLayoutIds(node, knownIds) {
|
|
105
|
+
return [
|
|
106
|
+
...(Array.isArray(node.views)
|
|
107
|
+
? node.views.filter((ref) => typeof ref === 'string' && knownIds.has(ref))
|
|
108
|
+
: []),
|
|
109
|
+
...(Array.isArray(node.children)
|
|
110
|
+
? node.children.flatMap(child => pinnedLayoutIds(child, knownIds))
|
|
111
|
+
: []),
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
// A layout index is a position in the spec's `views` array, so one past the end
|
|
115
|
+
// (or negative) is an authoring slip that would otherwise silently drop that
|
|
116
|
+
// view from the layout, or, if every reference in a panel is bad, leave an
|
|
117
|
+
// empty panel with no clue why. An id names a view pinned with the spec's `id`
|
|
118
|
+
// key, so one nothing created is the same slip.
|
|
119
|
+
function unresolvedLayoutRefs(node, viewCount, knownIds) {
|
|
120
|
+
// Array-guarded rather than `?.`: a hand-written `views: 0` is non-nullish,
|
|
121
|
+
// so `?.` does not short-circuit and `(0).filter` threw a raw TypeError out
|
|
122
|
+
// of this validator — past `resolveLayoutSpec`, which has a real message for
|
|
123
|
+
// that exact slip and never got to say it.
|
|
124
|
+
return [
|
|
125
|
+
...(Array.isArray(node.views)
|
|
126
|
+
? node.views.filter(ref => typeof ref === 'number'
|
|
127
|
+
? ref < 0 || ref >= viewCount
|
|
128
|
+
: !knownIds.has(ref))
|
|
129
|
+
: []),
|
|
130
|
+
...(Array.isArray(node.children)
|
|
131
|
+
? node.children.flatMap(child => unresolvedLayoutRefs(child, viewCount, knownIds))
|
|
132
|
+
: []),
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
// A `tabs` node shares one cell between its children rather than dividing the
|
|
136
|
+
// space, so a `size` on one of them describes nothing. This is the ONLY case
|
|
137
|
+
// left where a stated size is dropped.
|
|
138
|
+
//
|
|
139
|
+
// It used to be far wider — dockview forces its branches to alternate
|
|
140
|
+
// orientation by depth, so a nested container had no branch to size against and
|
|
141
|
+
// the whole sizing pass bailed, top-level numbers included. That is gone
|
|
142
|
+
// (ADR-068): the spec's nesting is the tree's nesting, `size` applies wherever
|
|
143
|
+
// it is written, and a bare sibling takes an equal share of the remainder. The
|
|
144
|
+
// wider check outlived the limitation it reported and told an author their
|
|
145
|
+
// nested or partial sizes had been ignored while the layout honoured them —
|
|
146
|
+
// which is also what website/docs/urlparams.md promises.
|
|
147
|
+
function unsizeableLayoutNodes(layout) {
|
|
148
|
+
const children = Array.isArray(layout.children) ? layout.children : [];
|
|
149
|
+
const sizedTabsChild = layout.direction === 'tabs' && children.some(c => c.size !== undefined);
|
|
150
|
+
return sizedTabsChild || children.some(unsizeableLayoutNodes);
|
|
151
|
+
}
|
|
152
|
+
// The other statement a `tabs` node cannot take literally, and the one that
|
|
153
|
+
// used to fail silently rather than partially: a tab holds a FLAT stack of
|
|
154
|
+
// views, so a container child has no split to become. Containers nest
|
|
155
|
+
// arbitrarily deep everywhere else — the docs say so — which is what makes one
|
|
156
|
+
// here easy to write. Its views are flattened into a single tab now; they used
|
|
157
|
+
// to be dropped from the layout, after which homing swept them into whichever
|
|
158
|
+
// tab happened to be showing, so the arrangement was wrong with nothing said.
|
|
159
|
+
function flattenedTabsContainers(layout) {
|
|
160
|
+
const children = Array.isArray(layout.children) ? layout.children : [];
|
|
161
|
+
const nestedTabsChild = layout.direction === 'tabs' && children.some(c => c.children !== undefined);
|
|
162
|
+
return nestedTabsChild || children.some(flattenedTabsContainers);
|
|
163
|
+
}
|
|
164
|
+
function unknownViewTypesMessage(types) {
|
|
165
|
+
return `Unknown view type(s) in session spec: ${types.join(', ')}. The plugin providing the view may be missing, or the type may be misspelled.`;
|
|
166
|
+
}
|
|
167
|
+
function noLauncherMessage(types) {
|
|
168
|
+
return `View type(s) ${types.join(', ')} cannot be launched from a session spec: no LaunchView extension point is registered for them.`;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Why one view type cannot launch from a spec here, or nothing. Two causes,
|
|
172
|
+
* two messages: the type is unknown at all (a typo, or a plugin that was not
|
|
173
|
+
* loaded), or it exists but nothing taught it how to launch from a spec, which
|
|
174
|
+
* only the view's plugin can fix.
|
|
175
|
+
*/
|
|
176
|
+
export function viewTypeProblem(pluginManager, type) {
|
|
177
|
+
if (pluginManager.extensionPoints.has(`LaunchView-${type}`)) {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
// the record's `has`, not getElementType, which throws on an unregistered
|
|
181
|
+
// name rather than returning undefined
|
|
182
|
+
return pluginManager.getElementTypeRecord('view').has(type)
|
|
183
|
+
? noLauncherMessage([type])
|
|
184
|
+
: unknownViewTypesMessage([type]);
|
|
185
|
+
}
|
|
186
|
+
// v4 nested a view's settings under `init`. A spec never becomes a snapshot, so
|
|
187
|
+
// `withLaunchInput`'s unwrap never runs on one and this surface does it itself,
|
|
188
|
+
// in the same order: the flat spelling wins.
|
|
189
|
+
function flattenSpecView(spec) {
|
|
190
|
+
const { init, ...view } = spec;
|
|
191
|
+
if (init) {
|
|
192
|
+
console.warn(legacyInitMessage(view.type));
|
|
193
|
+
}
|
|
194
|
+
return init ? { ...init, ...view } : view;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* A spec view entry as its launcher takes it, and what its type does not take.
|
|
198
|
+
*
|
|
199
|
+
* The same classification a view snapshot gets, run here because a spec never
|
|
200
|
+
* becomes one: `LaunchView-<type>` takes these keys as arguments, so
|
|
201
|
+
* `withLaunchInput`'s partition never sees them and nothing names a typo.
|
|
202
|
+
* `{type: 'LinearGenomeView', asembly: 'volvox'}` reported only the launcher's
|
|
203
|
+
* downstream "No assembly provided", on the one surface written by hand with no
|
|
204
|
+
* compiler and no editor behind it.
|
|
205
|
+
*
|
|
206
|
+
* A view type that registers no launch keys classifies nothing: its launcher's
|
|
207
|
+
* vocabulary is undeclared, so every argument would read as a typo. A lazily
|
|
208
|
+
* registered type declares its properties on a state model that is not loaded
|
|
209
|
+
* yet, so it is loaded first — the launch would have loaded it anyway.
|
|
210
|
+
*/
|
|
211
|
+
export async function launchableSpecView(pluginManager, spec) {
|
|
212
|
+
const flat = flattenSpecView(spec);
|
|
213
|
+
if (!pluginManager.getElementTypeRecord('view').has(flat.type)) {
|
|
214
|
+
return { view: flat };
|
|
215
|
+
}
|
|
216
|
+
const viewType = pluginManager.getViewType(flat.type);
|
|
217
|
+
await viewType.loadStateModel();
|
|
218
|
+
const accepted = viewType.acceptedKeys;
|
|
219
|
+
if (!accepted) {
|
|
220
|
+
return { view: flat };
|
|
221
|
+
}
|
|
222
|
+
const { view, problem } = readAssemblyNames(flat, accepted);
|
|
223
|
+
const { type, ...keyed } = view;
|
|
224
|
+
const unknown = Object.keys(keyed).filter(key => !accepted.includes(key));
|
|
225
|
+
const problems = [
|
|
226
|
+
...(unknown.length ? [unknownKeysMessage(type, unknown)] : []),
|
|
227
|
+
...(problem ? [problem] : []),
|
|
228
|
+
];
|
|
229
|
+
return { view, problem: problems.join('; ') || undefined };
|
|
230
|
+
}
|
|
231
|
+
// An agent reads `assemblyNames` off the live view and writes it back into a
|
|
232
|
+
// spec, where the key the entry takes is `assembly`. One name is that assembly;
|
|
233
|
+
// several are a question one view cannot answer.
|
|
234
|
+
function readAssemblyNames(view, accepted) {
|
|
235
|
+
const { assemblyNames, ...rest } = view;
|
|
236
|
+
// Array.isArray, not a length check: a hand-written `assemblyNames: 'volvox'`
|
|
237
|
+
// has a length too, and reached .join below — a throw, which this one outer
|
|
238
|
+
// catch turns into every remaining view and the layout being lost
|
|
239
|
+
if (!Array.isArray(assemblyNames) ||
|
|
240
|
+
!assemblyNames.length ||
|
|
241
|
+
!accepted.includes('assembly') ||
|
|
242
|
+
accepted.includes('assemblyNames')) {
|
|
243
|
+
return { view };
|
|
244
|
+
}
|
|
245
|
+
return assemblyNames.length === 1
|
|
246
|
+
? { view: { ...rest, assembly: rest.assembly ?? assemblyNames[0] } }
|
|
247
|
+
: {
|
|
248
|
+
view: rest,
|
|
249
|
+
problem: `${view.type} takes one "assembly", and assemblyNames names ${assemblyNames.length} (${assemblyNames.join(', ')}) — pass assembly, one view per assembly`,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Launch one spec view into the session through its `LaunchView-<type>`
|
|
254
|
+
* extension point, the same door `loadSessionSpec` uses for each of its views
|
|
255
|
+
* — so `jb.addView` and a spec agree on what a view entry means, a
|
|
256
|
+
* ProteinView's `connectedView` included.
|
|
257
|
+
*
|
|
258
|
+
* Strict so a launch handler that throws (missing/invalid assembly, unresolved
|
|
259
|
+
* track, ...) is returned as a failure instead of being swallowed by the plain
|
|
260
|
+
* extension-point runner, which would leave a silent empty session. A handler
|
|
261
|
+
* that legitimately no-ops returns normally and is unaffected.
|
|
262
|
+
*
|
|
263
|
+
* `created` is EVERY view the launch added, in creation order: a connected
|
|
264
|
+
* ProteinView creates its genome view and then itself. `view` is the entry's
|
|
265
|
+
* own, the one of the spec's type, which is where `displayName` goes — it used
|
|
266
|
+
* to land on the genome view a ProteinView opened first, and the structure came
|
|
267
|
+
* up "Untitled view". `type` is the dispatch key, not a setting: forwarding it
|
|
268
|
+
* would land in the view's launch blob and trip the "ignored unknown key(s):
|
|
269
|
+
* type" warning meant to catch typos.
|
|
270
|
+
*/
|
|
271
|
+
export async function launchSpecView(session, pluginManager, spec) {
|
|
272
|
+
const { type, displayName, ...view } = spec;
|
|
273
|
+
const before = new Set(session?.views.map(v => v.id));
|
|
274
|
+
let failure;
|
|
275
|
+
try {
|
|
276
|
+
await pluginManager.evaluateAsyncExtensionPointStrict(`LaunchView-${type}`, {
|
|
277
|
+
...view,
|
|
278
|
+
session,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
catch (e) {
|
|
282
|
+
console.error(e);
|
|
283
|
+
failure = { message: `Failed to launch ${type} view: ${e}`, cause: e };
|
|
284
|
+
}
|
|
285
|
+
const created = session?.views.filter(v => !before.has(v.id)) ?? [];
|
|
286
|
+
const named = created.find(v => v.type === type) ?? created[0];
|
|
287
|
+
if (named && displayName) {
|
|
288
|
+
named.setDisplayName(displayName);
|
|
289
|
+
}
|
|
290
|
+
return { created, view: named, ...(failure ? { failure } : {}) };
|
|
291
|
+
}
|
|
292
|
+
const SPEC_KEYS = [
|
|
293
|
+
'views',
|
|
294
|
+
'sessionAssemblies',
|
|
295
|
+
'sessionConnections',
|
|
296
|
+
'sessionTracks',
|
|
297
|
+
'layout',
|
|
298
|
+
'sessionName',
|
|
299
|
+
];
|
|
300
|
+
export async function loadSessionSpec(spec, pluginManager) {
|
|
301
|
+
const { views, sessionAssemblies = [], sessionConnections = [], sessionTracks = [], layout, sessionName, } = spec;
|
|
302
|
+
const rootModel = pluginManager.rootModel;
|
|
303
|
+
try {
|
|
304
|
+
rootModel.setSession?.({
|
|
305
|
+
name: sessionName ?? `New session ${new Date().toLocaleString()}`,
|
|
306
|
+
});
|
|
307
|
+
const { session } = rootModel;
|
|
308
|
+
// `tracks` for sessionTracks, `assemblies` for sessionAssemblies, `name`
|
|
309
|
+
// for sessionName: a top-level key this spec does not define used to be
|
|
310
|
+
// dropped by the destructure above with nothing said, on the one door every
|
|
311
|
+
// agent-facing text names first. Reported like the misplaced keys below,
|
|
312
|
+
// rather than thrown, so the rest of the spec still loads.
|
|
313
|
+
const unknownKeys = Object.keys(spec).filter(key => !SPEC_KEYS.includes(key));
|
|
314
|
+
if (unknownKeys.length) {
|
|
315
|
+
session?.notifyError(`${unknownKeysMessage('Session spec', unknownKeys)} — it takes ${SPEC_KEYS.join(', ')}`);
|
|
316
|
+
}
|
|
317
|
+
// Assemblies first: sessionTracks and the views below reference them by
|
|
318
|
+
// name, so a self-contained spec (novel assemblies + their tracks, no
|
|
319
|
+
// hosted config) resolves only if the assemblies exist before either runs.
|
|
320
|
+
if (isSessionWithAddAssembly(session)) {
|
|
321
|
+
// Per-assembly, like connections and tracks below: an invalid config
|
|
322
|
+
// fails the array's MST type check and would otherwise cost the spec
|
|
323
|
+
// everything after it
|
|
324
|
+
for (const assembly of sessionAssemblies) {
|
|
325
|
+
try {
|
|
326
|
+
session.addSessionAssembly(assembly);
|
|
327
|
+
}
|
|
328
|
+
catch (e) {
|
|
329
|
+
console.error(e);
|
|
330
|
+
const label = typeof assembly.name === 'string' ? assembly.name : '?';
|
|
331
|
+
session.notifyError(`Assembly "${label}" has an invalid configuration: ${e}`, e);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// Connections after the assemblies (a connection config may name one the
|
|
336
|
+
// spec defines) and before the tracks and views, which are what reference
|
|
337
|
+
// the assemblies and tracks a connection brings in.
|
|
338
|
+
if (sessionConnections.length) {
|
|
339
|
+
if (session && isSessionWithSpecConnections(session)) {
|
|
340
|
+
// A connection type nothing registered is a typo or a missing plugin,
|
|
341
|
+
// the same authoring slip the view check below reports. Caught here
|
|
342
|
+
// because the config would otherwise fail the connection array's MST
|
|
343
|
+
// union check and throw past every per-key guard to the catch at the
|
|
344
|
+
// bottom, costing the spec its tracks, its views and its layout over
|
|
345
|
+
// one bad entry — and reporting it as a raw union-type dump.
|
|
346
|
+
const connectionTypes = pluginManager.getElementTypeRecord('connection');
|
|
347
|
+
for (const conf of sessionConnections) {
|
|
348
|
+
const { type } = conf;
|
|
349
|
+
const label = typeof conf.connectionId === 'string' ? conf.connectionId : '?';
|
|
350
|
+
if (typeof type !== 'string' || !connectionTypes.has(type)) {
|
|
351
|
+
session.notifyError(`Session spec connection "${label}" has ${typeof type === 'string'
|
|
352
|
+
? `unknown type "${type}". The plugin providing the connection may be missing, or the type may be misspelled.`
|
|
353
|
+
: 'no "type".'}`);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
// Per-connection, for the same reason each view gets its own: one
|
|
357
|
+
// unusable connection shouldn't cost the spec everything after it.
|
|
358
|
+
try {
|
|
359
|
+
// silent whenever the spec launches views of its own: a connection
|
|
360
|
+
// that opens one on connect (a single-file UCSC hub goes to its
|
|
361
|
+
// `defaultPos`) would otherwise open a second view competing with
|
|
362
|
+
// the one the spec asked for. With no views the spec is only asking
|
|
363
|
+
// to attach the connection, so let it do its own thing.
|
|
364
|
+
session.makeConnection(session.addSessionConnectionConf
|
|
365
|
+
? session.addSessionConnectionConf(conf)
|
|
366
|
+
: session.addConnectionConf(conf), { silent: views.length > 0 });
|
|
367
|
+
}
|
|
368
|
+
catch (e) {
|
|
369
|
+
console.error(e);
|
|
370
|
+
session.notifyError(`Connection "${label}" has an invalid configuration: ${e}`, e);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
session?.notifyError('Session spec has "sessionConnections", but this application cannot add connections to a session');
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
addSessionTracks(session, sessionTracks);
|
|
379
|
+
// A view type with no registered LaunchView-<type> extension point makes
|
|
380
|
+
// evaluateAsyncExtensionPoint a silent no-op, leaving an empty session with
|
|
381
|
+
// no diagnostic. Reported once per type, not per view.
|
|
382
|
+
const viewTypes = pluginManager.getElementTypeRecord('view');
|
|
383
|
+
const notLaunchable = [
|
|
384
|
+
...new Set(views.flatMap(view => viewTypeProblem(pluginManager, view.type) ? [view.type] : [])),
|
|
385
|
+
];
|
|
386
|
+
const unknown = notLaunchable.filter(type => !viewTypes.has(type));
|
|
387
|
+
const noLauncher = notLaunchable.filter(type => viewTypes.has(type));
|
|
388
|
+
if (unknown.length) {
|
|
389
|
+
session?.notifyError(unknownViewTypesMessage(unknown));
|
|
390
|
+
}
|
|
391
|
+
if (noLauncher.length) {
|
|
392
|
+
session?.notifyError(noLauncherMessage(noLauncher));
|
|
393
|
+
}
|
|
394
|
+
// Let any connection this spec registered finish before the views that
|
|
395
|
+
// reference what it supplies are launched (see whenConnectionsSettle). Gated
|
|
396
|
+
// on there being views: with none, nothing is waiting on the connection, and
|
|
397
|
+
// holding the load open until a hub finishes fetching would be pure delay —
|
|
398
|
+
// the connection is the deliverable. A session that went away while we
|
|
399
|
+
// waited has nothing left to launch into.
|
|
400
|
+
if (sessionConnections.length &&
|
|
401
|
+
views.length &&
|
|
402
|
+
session &&
|
|
403
|
+
isSessionWithSpecConnections(session) &&
|
|
404
|
+
!(await whenConnectionsSettle(session))) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
// Launch sequentially and record the ids each spec view created, so the
|
|
408
|
+
// layout below can map its indices to real views. Reading session.views
|
|
409
|
+
// positionally afterwards only works while every handler happens to addView
|
|
410
|
+
// synchronously and in order; capturing the delta per launch instead is
|
|
411
|
+
// correct even if a handler awaits or adds an auxiliary view, and lets a
|
|
412
|
+
// later spec view (e.g. a connected MsaView) reference an earlier one that
|
|
413
|
+
// now already exists.
|
|
414
|
+
//
|
|
415
|
+
// An unknown key is an ERROR rather than the snapshot path's warning: this
|
|
416
|
+
// surface's misplaced keys already report as errors, and the launcher's own
|
|
417
|
+
// failure lands as an error a line later — a warning under it reads as the
|
|
418
|
+
// lesser of the two, when it is the cause. Per view, so one bad view
|
|
419
|
+
// doesn't cost the rest.
|
|
420
|
+
const createdViewIds = [];
|
|
421
|
+
for (const spec of views) {
|
|
422
|
+
const { view, problem } = await launchableSpecView(pluginManager, spec);
|
|
423
|
+
if (problem) {
|
|
424
|
+
session?.notifyError(problem);
|
|
425
|
+
}
|
|
426
|
+
const { created, failure } = await launchSpecView(session, pluginManager, view);
|
|
427
|
+
if (failure) {
|
|
428
|
+
session?.notifyError(failure.message, failure.cause);
|
|
429
|
+
}
|
|
430
|
+
createdViewIds.push(created.map(v => v.id));
|
|
431
|
+
}
|
|
432
|
+
if (layout && session) {
|
|
433
|
+
const knownIds = new Set(createdViewIds.flat());
|
|
434
|
+
const bad = unresolvedLayoutRefs(layout, views.length, knownIds);
|
|
435
|
+
const badIndices = bad.filter(ref => typeof ref === 'number');
|
|
436
|
+
const badIds = bad.filter(ref => typeof ref === 'string');
|
|
437
|
+
if (badIndices.length) {
|
|
438
|
+
session.notifyError(`Session spec layout references view index ${badIndices.join(', ')}, but the spec has ${views.length} view(s).`);
|
|
439
|
+
}
|
|
440
|
+
if (badIds.length) {
|
|
441
|
+
session.notifyError(`Session spec layout references view id ${badIds.map(id => `"${id}"`).join(', ')}, but no view in the spec has that id (a view's "id" key pins it).`);
|
|
442
|
+
}
|
|
443
|
+
if (unsizeableLayoutNodes(layout)) {
|
|
444
|
+
session.notify('Session spec layout: a "tabs" node shares one cell between its children rather than dividing the space, so the "size" on them was ignored. The rest of the layout is unaffected.', 'info');
|
|
445
|
+
}
|
|
446
|
+
if (flattenedTabsContainers(layout)) {
|
|
447
|
+
session.notify('Session spec layout: a "tabs" node gives each child one tab, and a tab holds a flat stack of views — so a container inside one was flattened into a single tab rather than splitting it. The rest of the layout is unaffected.', 'info');
|
|
448
|
+
}
|
|
449
|
+
if (hasLayoutViews(session)) {
|
|
450
|
+
// Its own try/catch, the same reasoning as the per-view one above: the
|
|
451
|
+
// resolver throws for a layout it will not arrange, and this is the
|
|
452
|
+
// LAST thing the spec does — so an unarrangeable layout used to take
|
|
453
|
+
// the whole load's catch and report itself as the session's error. The
|
|
454
|
+
// layout is one statement in the spec; losing it should cost the spec
|
|
455
|
+
// its layout and nothing else.
|
|
456
|
+
try {
|
|
457
|
+
session.layoutViews(convertLayoutNode(layout, createdViewIds, knownIds, new Set(pinnedLayoutIds(layout, knownIds))));
|
|
458
|
+
}
|
|
459
|
+
catch (e) {
|
|
460
|
+
console.error(e);
|
|
461
|
+
session.notifyError(`Session spec layout: ${e}`, e);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
session.notifyError('Session spec has a "layout", but this application does not support workspace layouts');
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
catch (e) {
|
|
470
|
+
console.error(e);
|
|
471
|
+
rootModel.session?.notifyError(`${e}`, e);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SessionSpec } from './loadSessionSpec.ts';
|
|
2
|
+
export interface ParsedSessionSpec {
|
|
3
|
+
configUrl?: string;
|
|
4
|
+
spec: Omit<SessionSpec, 'sessionName'>;
|
|
5
|
+
sessionName?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function parseSessionSpecUrl(input: string): ParsedSessionSpec;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { readAllQueryParams } from '../queryParams.js';
|
|
2
|
+
import { buildLgvInitFromParams, hubConnectionSpec, readHubUrlParam, } from './lgvUrlInit.js';
|
|
3
|
+
// A `jbrowse://open?url=<web link>` wrapper carries a JBrowse Web url for
|
|
4
|
+
// Desktop's protocol handler to unwrap. Every page that offers an "Open in
|
|
5
|
+
// Desktop" button puts that link right next to the plain web one — the docs'
|
|
6
|
+
// figure dialogs, genomes.jbrowse.org — so the link someone copies and pastes
|
|
7
|
+
// into "Open JBrowse Web link..." is as often this as the url inside it. It
|
|
8
|
+
// parses as a perfectly good URL whose query has no `session`, which reported
|
|
9
|
+
// "that link has no session in it" about a link that does. Unwrap it instead.
|
|
10
|
+
//
|
|
11
|
+
// Restricted to http(s), and to a single unwrap, for the reason Desktop's own
|
|
12
|
+
// parser is (products/jbrowse-desktop/electron/launchTarget.ts, which gates the
|
|
13
|
+
// OS-delivered copy of this and cannot import from here — it is bundled into the
|
|
14
|
+
// Electron main process): the payload comes from whatever handed us the link.
|
|
15
|
+
function unwrapProtocolUrl(url) {
|
|
16
|
+
if (url.protocol !== 'jbrowse:') {
|
|
17
|
+
return url;
|
|
18
|
+
}
|
|
19
|
+
const wrapped = url.searchParams.get('url');
|
|
20
|
+
if (!wrapped) {
|
|
21
|
+
return url;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const inner = new URL(wrapped);
|
|
25
|
+
return inner.protocol === 'http:' || inner.protocol === 'https:'
|
|
26
|
+
? inner
|
|
27
|
+
: url;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return url;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function resolveConfig(params, url) {
|
|
34
|
+
const config = params.get('config');
|
|
35
|
+
return {
|
|
36
|
+
// a relative config (e.g. `test_data/volvox/config.json`) is served by the
|
|
37
|
+
// instance the link points at, so resolve it there rather than handing back
|
|
38
|
+
// a path nothing outside that instance could fetch.
|
|
39
|
+
//
|
|
40
|
+
// `config=none` is jbrowse-web's "load no config at all" sentinel
|
|
41
|
+
// (SessionLoader.fetchConfig), and the form a self-contained spec link has to
|
|
42
|
+
// take — omitting `config` entirely makes web fall back to its own
|
|
43
|
+
// config.json. Treated as no config here, not resolved, which would hand the
|
|
44
|
+
// caller `<instance>/none` to fetch.
|
|
45
|
+
configUrl: config && config !== 'none' ? new URL(config, url.href).href : undefined,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The spec a link with no `session=` describes — or the error explaining why it
|
|
50
|
+
* describes none.
|
|
51
|
+
*
|
|
52
|
+
* The branches below are in jbrowse-web's own ranking (SessionLoader's
|
|
53
|
+
* loadSessionByType), and have to be, because a link can carry several of these
|
|
54
|
+
* at once and which one wins decides what the link means:
|
|
55
|
+
*
|
|
56
|
+
* 1. `&extendSession=true` alongside the shorthand, which outranks even a hub.
|
|
57
|
+
* 2. `&hubURL=`, above the bare shorthand, because a hub is the only param that
|
|
58
|
+
* brings its own assemblies and tracks — a link carrying both is asking to
|
|
59
|
+
* navigate *inside* the hub. Ranking the shorthand first builds a bare LGV
|
|
60
|
+
* and drops the hub with no diagnostic.
|
|
61
|
+
* 3. the shorthand on its own.
|
|
62
|
+
*/
|
|
63
|
+
function shorthandSpec(params) {
|
|
64
|
+
const init = buildLgvInitFromParams(params);
|
|
65
|
+
// what makes a link a jb1-style one, the same test as jbrowse-web's
|
|
66
|
+
// isJb1StyleSession. The rest of the shorthand only modifies a view; none of
|
|
67
|
+
// it can conjure one.
|
|
68
|
+
const isJb1Style = init.loc !== undefined || init.assembly !== undefined;
|
|
69
|
+
// `&extendSession=true` means "apply this shorthand to the config's own
|
|
70
|
+
// defaultSession", and there is no honest way to do that here: this builds a
|
|
71
|
+
// session spec, and loadSessionSpec replaces the session rather than
|
|
72
|
+
// navigating one the config supplied. Opening the view without the
|
|
73
|
+
// defaultSession's curated tracks would quietly be a different session than
|
|
74
|
+
// the link asks for, so say so instead — and say what to remove, since the
|
|
75
|
+
// link works fine without it.
|
|
76
|
+
//
|
|
77
|
+
// Gated on the shorthand being present for the same reason web's branch is:
|
|
78
|
+
// the param names the default session as the thing to navigate, and with no
|
|
79
|
+
// loc or assembly there is no navigation to apply to it.
|
|
80
|
+
if (isJb1Style && params.get('extendSession') === 'true') {
|
|
81
|
+
throw new Error('That link uses "&extendSession=true", which layers its location onto a defaultSession that only the JBrowse Web instance it points at can supply. Remove that parameter to open just the view the link describes.');
|
|
82
|
+
}
|
|
83
|
+
const hubURL = readHubUrlParam(params.get('hubURL') ?? undefined);
|
|
84
|
+
if (hubURL.length) {
|
|
85
|
+
// A track hub is a session connection, which a spec has carried since
|
|
86
|
+
// sessionConnections landed. So this is no longer the dead end it was
|
|
87
|
+
// ("that's a track hub, go add it by hand") — the hub attaches, and
|
|
88
|
+
// loadSessionSpec waits for it to settle before launching anything, because
|
|
89
|
+
// the assemblies and tracks the view below names are ones the hub brings.
|
|
90
|
+
return {
|
|
91
|
+
sessionConnections: hubURL.map(hubConnectionSpec),
|
|
92
|
+
sessionTracks: readSessionTracks(params),
|
|
93
|
+
// Only with an assembly. The launcher resolves everything against one,
|
|
94
|
+
// and a hub's assemblies are genome ids only the hub carries, so `&loc=`
|
|
95
|
+
// alone has nothing to launch against. Leaving the list empty is what
|
|
96
|
+
// makes loadSessionSpec register the connection non-silently, so the hub
|
|
97
|
+
// opens at its own defaultPos — the best guess available without a name,
|
|
98
|
+
// and what jbrowse-web does with the same link.
|
|
99
|
+
views: init.assembly === undefined ? [] : [lgvSpec(init)],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (!isJb1Style) {
|
|
103
|
+
throw new Error('That link has no session in it. Copy a JBrowse Web link that contains "&session=spec-...", or one with "&assembly=" and "&loc=".');
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
// `&sessionTracks=` carries whole track configs the views then reference by
|
|
107
|
+
// id, exactly as in a spec link
|
|
108
|
+
sessionTracks: readSessionTracks(params),
|
|
109
|
+
views: [lgvSpec(init)],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// The init keys are LGV `InitState` ones the LaunchView-LinearGenomeView
|
|
113
|
+
// extension point partitions for itself; ViewSpec carries only what every view
|
|
114
|
+
// type has, which is why this spreads rather than naming them.
|
|
115
|
+
function lgvSpec(init) {
|
|
116
|
+
return { type: 'LinearGenomeView', ...init };
|
|
117
|
+
}
|
|
118
|
+
function readSessionTracks(params) {
|
|
119
|
+
const sessionTracks = params.get('sessionTracks');
|
|
120
|
+
if (!sessionTracks) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
let parsed;
|
|
124
|
+
try {
|
|
125
|
+
parsed = JSON.parse(sessionTracks);
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
throw new Error(`The "sessionTracks" in that link isn't valid JSON: ${e}`, {
|
|
129
|
+
cause: e,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (!Array.isArray(parsed)) {
|
|
133
|
+
throw new Error('The "sessionTracks" in that link is not a list of track configurations.');
|
|
134
|
+
}
|
|
135
|
+
return parsed;
|
|
136
|
+
}
|
|
137
|
+
export function parseSessionSpecUrl(input) {
|
|
138
|
+
let url;
|
|
139
|
+
try {
|
|
140
|
+
url = unwrapProtocolUrl(new URL(input.trim()));
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
throw new Error(`Not a URL: ${input}`);
|
|
144
|
+
}
|
|
145
|
+
const params = readAllQueryParams(url);
|
|
146
|
+
const session = params.get('session');
|
|
147
|
+
if (!session) {
|
|
148
|
+
return {
|
|
149
|
+
...resolveConfig(params, url),
|
|
150
|
+
spec: shorthandSpec(params),
|
|
151
|
+
sessionName: params.get('sessionName') ?? undefined,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (!session.startsWith('spec-')) {
|
|
155
|
+
const kind = /^(share|encoded|local|json)-/.exec(session)?.[1];
|
|
156
|
+
throw new Error(kind
|
|
157
|
+
? `This is a "${kind}" session link, which only the JBrowse Web instance that created it can open. Session-spec links (&session=spec-...) work here.`
|
|
158
|
+
: 'Unrecognized session in that link; only session-spec links (&session=spec-...) can be opened here.');
|
|
159
|
+
}
|
|
160
|
+
let spec;
|
|
161
|
+
try {
|
|
162
|
+
spec = JSON.parse(session.slice('spec-'.length));
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
throw new Error(`The session spec in that link isn't valid JSON: ${e}`, {
|
|
166
|
+
cause: e,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
// `views: []` is legitimate and deliberate — it opens a session showing the
|
|
170
|
+
// import form (several docs figures are exactly that). Only a spec with no
|
|
171
|
+
// views *key* is malformed, and would otherwise build an empty session that
|
|
172
|
+
// reads as a silent failure.
|
|
173
|
+
if (!Array.isArray(spec.views)) {
|
|
174
|
+
throw new Error('The session spec in that link has no "views" list, so there is nothing to open.');
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
...resolveConfig(params, url),
|
|
178
|
+
spec,
|
|
179
|
+
sessionName: params.get('sessionName') ?? undefined,
|
|
180
|
+
};
|
|
181
|
+
}
|