@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.
Files changed (165) hide show
  1. package/esm/AppSession/AppRootModel.d.ts +30 -0
  2. package/esm/AppSession/AppRootModel.js +1 -0
  3. package/esm/AppSession/AppSessionMixin.d.ts +1324 -0
  4. package/esm/AppSession/AppSessionMixin.js +62 -0
  5. package/esm/AppSession/index.d.ts +2 -0
  6. package/esm/AppSession/index.js +1 -0
  7. package/esm/Assemblies/AssembliesMixin.d.ts +294 -0
  8. package/esm/Assemblies/AssembliesMixin.js +154 -0
  9. package/esm/Assemblies/index.d.ts +1 -2
  10. package/esm/Assemblies/index.js +1 -2
  11. package/esm/HistoryManagement/index.d.ts +11 -0
  12. package/esm/HistoryManagement/index.js +43 -10
  13. package/esm/JBrowseConfig/RootConfiguration.d.ts +123 -78
  14. package/esm/JBrowseConfig/RootConfiguration.js +79 -7
  15. package/esm/JBrowseConfig/defaultShareUrl.d.ts +1 -0
  16. package/esm/JBrowseConfig/defaultShareUrl.js +5 -0
  17. package/esm/JBrowseConfig/index.d.ts +175 -84
  18. package/esm/JBrowseConfig/index.js +64 -2
  19. package/esm/JBrowseModel/index.d.ts +175 -97
  20. package/esm/JBrowseModel/index.js +132 -31
  21. package/esm/JbApi/index.d.ts +2 -0
  22. package/esm/JbApi/index.js +1 -0
  23. package/esm/JbApi/jbApi.d.ts +2767 -0
  24. package/esm/JbApi/jbApi.js +1383 -0
  25. package/esm/RootMenu/index.d.ts +69 -3
  26. package/esm/RootMenu/index.js +96 -61
  27. package/esm/SessionSpec/index.d.ts +6 -0
  28. package/esm/SessionSpec/index.js +3 -0
  29. package/esm/SessionSpec/lgvUrlInit.d.ts +79 -0
  30. package/esm/SessionSpec/lgvUrlInit.js +161 -0
  31. package/esm/SessionSpec/loadSessionSpec.d.ts +75 -0
  32. package/esm/SessionSpec/loadSessionSpec.js +473 -0
  33. package/esm/SessionSpec/parseSessionSpecUrl.d.ts +7 -0
  34. package/esm/SessionSpec/parseSessionSpecUrl.js +181 -0
  35. package/esm/SessionSpec/types.d.ts +37 -0
  36. package/esm/SessionSpec/types.js +1 -0
  37. package/esm/WorkspaceLayout/LayoutRenderer.d.ts +24 -0
  38. package/esm/WorkspaceLayout/LayoutRenderer.js +185 -0
  39. package/esm/WorkspaceLayout/PanelView.d.ts +13 -0
  40. package/esm/WorkspaceLayout/PanelView.js +99 -0
  41. package/esm/WorkspaceLayout/TabStrip.d.ts +14 -0
  42. package/esm/WorkspaceLayout/TabStrip.js +226 -0
  43. package/esm/WorkspaceLayout/WorkspaceContainer.d.ts +16 -0
  44. package/esm/WorkspaceLayout/WorkspaceContainer.js +107 -0
  45. package/esm/WorkspaceLayout/WorkspacePanelActions.d.ts +24 -0
  46. package/esm/WorkspaceLayout/WorkspacePanelActions.js +120 -0
  47. package/esm/WorkspaceLayout/WorkspaceTab.d.ts +27 -0
  48. package/esm/WorkspaceLayout/WorkspaceTab.js +105 -0
  49. package/esm/WorkspaceLayout/dockviewTheme.d.ts +69 -0
  50. package/esm/WorkspaceLayout/dockviewTheme.js +82 -0
  51. package/esm/WorkspaceLayout/dropZone.d.ts +105 -0
  52. package/esm/WorkspaceLayout/dropZone.js +113 -0
  53. package/esm/WorkspaceLayout/index.d.ts +8 -0
  54. package/esm/WorkspaceLayout/index.js +4 -0
  55. package/esm/WorkspaceLayout/model.d.ts +260 -0
  56. package/esm/WorkspaceLayout/model.js +586 -0
  57. package/esm/WorkspaceLayout/panelChrome.d.ts +19 -0
  58. package/esm/WorkspaceLayout/panelChrome.js +4 -0
  59. package/esm/WorkspaceLayout/spec.d.ts +88 -0
  60. package/esm/WorkspaceLayout/spec.js +226 -0
  61. package/esm/WorkspaceLayout/splitter.d.ts +25 -0
  62. package/esm/WorkspaceLayout/splitter.js +37 -0
  63. package/esm/WorkspaceLayout/tree.d.ts +158 -0
  64. package/esm/WorkspaceLayout/tree.js +379 -0
  65. package/esm/WorkspaceLayout/useLayoutDrag.d.ts +34 -0
  66. package/esm/WorkspaceLayout/useLayoutDrag.js +232 -0
  67. package/esm/index.d.ts +18 -9
  68. package/esm/index.js +16 -9
  69. package/esm/menus.d.ts +55 -68
  70. package/esm/menus.js +117 -86
  71. package/esm/queryParams.d.ts +9 -0
  72. package/esm/queryParams.js +93 -0
  73. package/esm/sessionModelFactory.d.ts +18 -0
  74. package/esm/sessionModelFactory.js +1 -0
  75. package/esm/ui/App/App.d.ts +3 -14
  76. package/esm/ui/App/App.js +39 -19
  77. package/esm/ui/App/AppFab.d.ts +12 -3
  78. package/esm/ui/App/AppFab.js +15 -8
  79. package/esm/ui/App/AppToolbar.d.ts +2 -14
  80. package/esm/ui/App/AppToolbar.js +45 -31
  81. package/esm/ui/App/ClassicViewsContainer.d.ts +4 -10
  82. package/esm/ui/App/ClassicViewsContainer.js +9 -8
  83. package/esm/ui/App/DialogQueue.d.ts +17 -1
  84. package/esm/ui/App/DialogQueue.js +30 -1
  85. package/esm/ui/App/Drawer.d.ts +1 -1
  86. package/esm/ui/App/Drawer.js +10 -39
  87. package/esm/ui/App/DrawerHeader.d.ts +2 -3
  88. package/esm/ui/App/DrawerHeader.js +24 -11
  89. package/esm/ui/App/DrawerHeaderHelpButton.d.ts +1 -1
  90. package/esm/ui/App/DrawerHeaderHelpButton.js +3 -3
  91. package/esm/ui/App/DrawerHeaderHelpDialog.d.ts +2 -2
  92. package/esm/ui/App/DrawerHeaderHelpDialog.js +3 -4
  93. package/esm/ui/App/DrawerWidget.d.ts +1 -1
  94. package/esm/ui/App/DrawerWidget.js +9 -21
  95. package/esm/ui/App/JBrowseTabMenu.d.ts +2 -3
  96. package/esm/ui/App/JBrowseTabMenu.js +24 -5
  97. package/esm/ui/App/ThemeModeButton.d.ts +12 -0
  98. package/esm/ui/App/ThemeModeButton.js +34 -0
  99. package/esm/ui/App/ViewButtons.d.ts +2 -4
  100. package/esm/ui/App/ViewButtons.js +15 -3
  101. package/esm/ui/App/ViewContainer.d.ts +3 -2
  102. package/esm/ui/App/ViewContainer.js +128 -24
  103. package/esm/ui/App/ViewContainerTitle.d.ts +1 -1
  104. package/esm/ui/App/ViewContainerTitle.js +21 -11
  105. package/esm/ui/App/ViewErrorFallback.d.ts +18 -0
  106. package/esm/ui/App/ViewErrorFallback.js +42 -0
  107. package/esm/ui/App/ViewHeader.d.ts +3 -4
  108. package/esm/ui/App/ViewHeader.js +28 -17
  109. package/esm/ui/App/ViewLauncher.d.ts +2 -8
  110. package/esm/ui/App/ViewLauncher.js +7 -5
  111. package/esm/ui/App/ViewMenu.d.ts +3 -5
  112. package/esm/ui/App/ViewMenu.js +87 -92
  113. package/esm/ui/App/ViewStack.d.ts +7 -0
  114. package/esm/ui/App/ViewStack.js +48 -0
  115. package/esm/ui/App/ViewWrapper.d.ts +15 -1
  116. package/esm/ui/App/ViewWrapper.js +26 -10
  117. package/esm/ui/App/ViewsContainer.d.ts +4 -11
  118. package/esm/ui/App/ViewsContainer.js +6 -9
  119. package/esm/ui/App/index.d.ts +2 -1
  120. package/esm/ui/App/index.js +3 -2
  121. package/esm/ui/App/lazyChunk.d.ts +13 -0
  122. package/esm/ui/App/lazyChunk.js +91 -0
  123. package/esm/ui/App/lazyParts.d.ts +32 -0
  124. package/esm/ui/App/lazyParts.js +8 -0
  125. package/esm/ui/App/preloadAppShell.d.ts +14 -0
  126. package/esm/ui/App/preloadAppShell.js +41 -0
  127. package/esm/ui/App/types.d.ts +17 -3
  128. package/esm/ui/App/types.js +1 -1
  129. package/esm/ui/App/useViewVisibility.d.ts +35 -0
  130. package/esm/ui/App/useViewVisibility.js +88 -0
  131. package/esm/ui/App/viewTitle.d.ts +12 -0
  132. package/esm/ui/App/viewTitle.js +17 -0
  133. package/esm/ui/index.d.ts +1 -1
  134. package/esm/ui/index.js +1 -1
  135. package/package.json +12 -17
  136. package/esm/AppFocus/index.d.ts +0 -5
  137. package/esm/AppFocus/index.js +0 -12
  138. package/esm/Assemblies/SessionAssembliesMixin.d.ts +0 -92
  139. package/esm/Assemblies/SessionAssembliesMixin.js +0 -53
  140. package/esm/Assemblies/TemporaryAssembliesMixin.d.ts +0 -10
  141. package/esm/Assemblies/TemporaryAssembliesMixin.js +0 -37
  142. package/esm/DockviewLayout/index.d.ts +0 -35
  143. package/esm/DockviewLayout/index.js +0 -111
  144. package/esm/ui/App/DockviewContext.d.ts +0 -22
  145. package/esm/ui/App/DockviewContext.js +0 -29
  146. package/esm/ui/App/DockviewLeftHeaderActions.d.ts +0 -2
  147. package/esm/ui/App/DockviewLeftHeaderActions.js +0 -58
  148. package/esm/ui/App/DockviewRightHeaderActions.d.ts +0 -2
  149. package/esm/ui/App/DockviewRightHeaderActions.js +0 -100
  150. package/esm/ui/App/DrawerControls.d.ts +0 -5
  151. package/esm/ui/App/DrawerControls.js +0 -25
  152. package/esm/ui/App/DrawerWidgetSelector.d.ts +0 -5
  153. package/esm/ui/App/DrawerWidgetSelector.js +0 -47
  154. package/esm/ui/App/JBrowseViewPanel.d.ts +0 -8
  155. package/esm/ui/App/JBrowseViewPanel.js +0 -40
  156. package/esm/ui/App/JBrowseViewTab.d.ts +0 -8
  157. package/esm/ui/App/JBrowseViewTab.js +0 -97
  158. package/esm/ui/App/ModalWidget.d.ts +0 -6
  159. package/esm/ui/App/ModalWidget.js +0 -48
  160. package/esm/ui/App/TiledViewsContainer.d.ts +0 -6
  161. package/esm/ui/App/TiledViewsContainer.js +0 -366
  162. package/esm/ui/App/copyView.d.ts +0 -1
  163. package/esm/ui/App/copyView.js +0 -29
  164. package/esm/ui/App/dockviewUtils.d.ts +0 -46
  165. 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
+ }