@jbrowse/app-core 5.0.0-beta.1 → 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 (75) hide show
  1. package/esm/AppSession/AppSessionMixin.d.ts +42 -316
  2. package/esm/Assemblies/AssembliesMixin.d.ts +172 -103
  3. package/esm/Assemblies/AssembliesMixin.js +13 -8
  4. package/esm/HistoryManagement/index.d.ts +3 -11
  5. package/esm/HistoryManagement/index.js +24 -28
  6. package/esm/JBrowseConfig/RootConfiguration.d.ts +80 -124
  7. package/esm/JBrowseConfig/RootConfiguration.js +5 -11
  8. package/esm/JBrowseConfig/defaultShareUrl.d.ts +1 -0
  9. package/esm/JBrowseConfig/defaultShareUrl.js +5 -0
  10. package/esm/JBrowseConfig/index.d.ts +89 -83
  11. package/esm/JBrowseConfig/index.js +6 -4
  12. package/esm/JBrowseModel/index.d.ts +93 -96
  13. package/esm/JBrowseModel/index.js +43 -43
  14. package/esm/JbApi/index.d.ts +2 -0
  15. package/esm/JbApi/index.js +1 -0
  16. package/esm/JbApi/jbApi.d.ts +2767 -0
  17. package/esm/JbApi/jbApi.js +1383 -0
  18. package/esm/RootMenu/index.d.ts +2 -11
  19. package/esm/RootMenu/index.js +0 -11
  20. package/esm/SessionSpec/index.d.ts +1 -1
  21. package/esm/SessionSpec/index.js +1 -1
  22. package/esm/SessionSpec/lgvUrlInit.js +5 -2
  23. package/esm/SessionSpec/loadSessionSpec.d.ts +56 -2
  24. package/esm/SessionSpec/loadSessionSpec.js +285 -123
  25. package/esm/SessionSpec/parseSessionSpecUrl.d.ts +2 -8
  26. package/esm/SessionSpec/parseSessionSpecUrl.js +2 -17
  27. package/esm/SessionSpec/types.d.ts +16 -12
  28. package/esm/WorkspaceLayout/WorkspaceContainer.js +4 -2
  29. package/esm/WorkspaceLayout/WorkspaceTab.js +2 -6
  30. package/esm/WorkspaceLayout/model.d.ts +24 -4
  31. package/esm/WorkspaceLayout/model.js +76 -17
  32. package/esm/WorkspaceLayout/spec.d.ts +40 -10
  33. package/esm/WorkspaceLayout/spec.js +81 -8
  34. package/esm/index.d.ts +4 -2
  35. package/esm/index.js +4 -1
  36. package/esm/menus.d.ts +15 -29
  37. package/esm/menus.js +76 -92
  38. package/esm/queryParams.d.ts +1 -1
  39. package/esm/queryParams.js +10 -9
  40. package/esm/ui/App/App.js +18 -11
  41. package/esm/ui/App/AppToolbar.js +23 -8
  42. package/esm/ui/App/ClassicViewsContainer.d.ts +2 -2
  43. package/esm/ui/App/DialogQueue.d.ts +2 -2
  44. package/esm/ui/App/DialogQueue.js +3 -3
  45. package/esm/ui/App/DrawerHeader.js +2 -3
  46. package/esm/ui/App/ThemeModeButton.d.ts +12 -0
  47. package/esm/ui/App/ThemeModeButton.js +34 -0
  48. package/esm/ui/App/ViewButtons.js +2 -2
  49. package/esm/ui/App/ViewContainer.js +1 -17
  50. package/esm/ui/App/ViewContainerTitle.js +15 -12
  51. package/esm/ui/App/ViewLauncher.js +1 -1
  52. package/esm/ui/App/ViewMenu.js +2 -2
  53. package/esm/ui/App/ViewStack.js +7 -1
  54. package/esm/ui/App/ViewsContainer.d.ts +2 -4
  55. package/esm/ui/App/ViewsContainer.js +3 -9
  56. package/esm/ui/App/index.d.ts +1 -1
  57. package/esm/ui/App/index.js +1 -1
  58. package/esm/ui/App/lazyParts.d.ts +32 -0
  59. package/esm/ui/App/lazyParts.js +8 -0
  60. package/esm/ui/App/preloadAppShell.d.ts +14 -0
  61. package/esm/ui/App/preloadAppShell.js +41 -0
  62. package/esm/ui/App/types.d.ts +12 -7
  63. package/esm/ui/App/useViewVisibility.d.ts +9 -8
  64. package/esm/ui/App/useViewVisibility.js +11 -10
  65. package/esm/ui/App/viewTitle.d.ts +10 -3
  66. package/esm/ui/App/viewTitle.js +13 -5
  67. package/esm/ui/index.d.ts +1 -1
  68. package/esm/ui/index.js +1 -1
  69. package/package.json +7 -7
  70. package/esm/ui/App/AppReadyMarker.d.ts +0 -34
  71. package/esm/ui/App/AppReadyMarker.js +0 -56
  72. package/esm/ui/App/DrawerControls.d.ts +0 -1
  73. package/esm/ui/App/DrawerControls.js +0 -1
  74. package/esm/ui/App/copyView.d.ts +0 -1
  75. package/esm/ui/App/copyView.js +0 -29
@@ -1,20 +1,11 @@
1
- import type { MenuAction, MenuDefinition } from '../menus.ts';
2
- import type { MenuItem } from '@jbrowse/core/ui/Menu';
1
+ import type { MenuAction } from '../menus.ts';
2
+ import type { MenuItem } from '@jbrowse/core/ui/menuItems';
3
3
  /**
4
4
  * #stateModel RootAppMenuMixin
5
5
  */
6
6
  export declare function RootAppMenuMixin(): import("@jbrowse/mobx-state-tree").IModelType<{}, {
7
7
  mutableMenuActions: MenuAction[];
8
8
  } & {
9
- /**
10
- * #action
11
- * Replace the menu bar wholesale. Item contributions recorded before
12
- * this one are dropped along with the menus they targeted, so a plugin
13
- * adding to the existing bar wants `appendToMenu` instead.
14
- *
15
- * @param newMenus - The complete new set of top-level menus.
16
- */
17
- setMenus(newMenus: MenuDefinition[]): void;
18
9
  /**
19
10
  * #action
20
11
  * Add a top-level menu, if the app bar does not already have one with
@@ -14,17 +14,6 @@ export function RootAppMenuMixin() {
14
14
  self.mutableMenuActions = [...self.mutableMenuActions, action];
15
15
  }
16
16
  return {
17
- /**
18
- * #action
19
- * Replace the menu bar wholesale. Item contributions recorded before
20
- * this one are dropped along with the menus they targeted, so a plugin
21
- * adding to the existing bar wants `appendToMenu` instead.
22
- *
23
- * @param newMenus - The complete new set of top-level menus.
24
- */
25
- setMenus(newMenus) {
26
- pushAction({ type: 'setMenus', newMenus });
27
- },
28
17
  /**
29
18
  * #action
30
19
  * Add a top-level menu, if the app bar does not already have one with
@@ -1,5 +1,5 @@
1
1
  export { buildLgvInit, buildLgvInitFromParams, hubConnectionSpec, readHubUrlParam, readNavParam, readTracklistParam, shortHubLabel, splitHighlights, } from './lgvUrlInit.ts';
2
- export { addSessionTracks, loadSessionSpec } from './loadSessionSpec.ts';
2
+ export { addSessionTracks, launchSpecView, launchableSpecView, loadSessionSpec, viewTypeProblem, } from './loadSessionSpec.ts';
3
3
  export { parseSessionSpecUrl } from './parseSessionSpecUrl.ts';
4
4
  export type { LgvUrlInit } from './lgvUrlInit.ts';
5
5
  export type { ParsedSessionSpec } from './parseSessionSpecUrl.ts';
@@ -1,3 +1,3 @@
1
1
  export { buildLgvInit, buildLgvInitFromParams, hubConnectionSpec, readHubUrlParam, readNavParam, readTracklistParam, shortHubLabel, splitHighlights, } from './lgvUrlInit.js';
2
- export { addSessionTracks, loadSessionSpec } from './loadSessionSpec.js';
2
+ export { addSessionTracks, launchSpecView, launchableSpecView, loadSessionSpec, viewTypeProblem, } from './loadSessionSpec.js';
3
3
  export { parseSessionSpecUrl } from './parseSessionSpecUrl.js';
@@ -66,8 +66,11 @@ export function buildLgvInit(args) {
66
66
  if (assembly !== undefined) {
67
67
  init.assembly = assembly;
68
68
  }
69
- if (tracks !== undefined) {
70
- init.tracks = tracks.split(',');
69
+ // empty entries dropped, like `regions`: `&tracks=a,` asked for no track
70
+ // named ''
71
+ const trackIds = tracks?.split(',').filter(Boolean);
72
+ if (trackIds?.length) {
73
+ init.tracks = trackIds;
71
74
  }
72
75
  if (tracklist !== undefined) {
73
76
  init.tracklist = tracklist;
@@ -11,11 +11,65 @@ import type { AbstractSessionModel } from '@jbrowse/core/util';
11
11
  * layer down.
12
12
  */
13
13
  export declare function addSessionTracks(session: AbstractSessionModel | undefined, tracks: Record<string, unknown>[]): void;
14
- export declare function loadSessionSpec({ views, sessionAssemblies, sessionConnections, sessionTracks, layout, sessionName, }: {
14
+ /**
15
+ * Why one view type cannot launch from a spec here, or nothing. Two causes,
16
+ * two messages: the type is unknown at all (a typo, or a plugin that was not
17
+ * loaded), or it exists but nothing taught it how to launch from a spec, which
18
+ * only the view's plugin can fix.
19
+ */
20
+ export declare function viewTypeProblem(pluginManager: PluginManager, type: string): string | undefined;
21
+ /**
22
+ * A spec view entry as its launcher takes it, and what its type does not take.
23
+ *
24
+ * The same classification a view snapshot gets, run here because a spec never
25
+ * becomes one: `LaunchView-<type>` takes these keys as arguments, so
26
+ * `withLaunchInput`'s partition never sees them and nothing names a typo.
27
+ * `{type: 'LinearGenomeView', asembly: 'volvox'}` reported only the launcher's
28
+ * downstream "No assembly provided", on the one surface written by hand with no
29
+ * compiler and no editor behind it.
30
+ *
31
+ * A view type that registers no launch keys classifies nothing: its launcher's
32
+ * vocabulary is undeclared, so every argument would read as a typo. A lazily
33
+ * registered type declares its properties on a state model that is not loaded
34
+ * yet, so it is loaded first — the launch would have loaded it anyway.
35
+ */
36
+ export declare function launchableSpecView(pluginManager: PluginManager, spec: ViewSpec): Promise<{
37
+ view: ViewSpec;
38
+ problem?: string;
39
+ }>;
40
+ /**
41
+ * Launch one spec view into the session through its `LaunchView-<type>`
42
+ * extension point, the same door `loadSessionSpec` uses for each of its views
43
+ * — so `jb.addView` and a spec agree on what a view entry means, a
44
+ * ProteinView's `connectedView` included.
45
+ *
46
+ * Strict so a launch handler that throws (missing/invalid assembly, unresolved
47
+ * track, ...) is returned as a failure instead of being swallowed by the plain
48
+ * extension-point runner, which would leave a silent empty session. A handler
49
+ * that legitimately no-ops returns normally and is unaffected.
50
+ *
51
+ * `created` is EVERY view the launch added, in creation order: a connected
52
+ * ProteinView creates its genome view and then itself. `view` is the entry's
53
+ * own, the one of the spec's type, which is where `displayName` goes — it used
54
+ * to land on the genome view a ProteinView opened first, and the structure came
55
+ * up "Untitled view". `type` is the dispatch key, not a setting: forwarding it
56
+ * would land in the view's launch blob and trip the "ignored unknown key(s):
57
+ * type" warning meant to catch typos.
58
+ */
59
+ export declare function launchSpecView(session: AbstractSessionModel | undefined, pluginManager: PluginManager, spec: ViewSpec): Promise<{
60
+ created: import("@jbrowse/core/util").AbstractViewModel[];
61
+ view: import("@jbrowse/core/util").AbstractViewModel | undefined;
62
+ failure?: {
63
+ message: string;
64
+ cause: unknown;
65
+ } | undefined;
66
+ }>;
67
+ export interface SessionSpec {
15
68
  views: ViewSpec[];
16
69
  sessionAssemblies?: Record<string, unknown>[];
17
70
  sessionConnections?: Record<string, unknown>[];
18
71
  sessionTracks?: Record<string, unknown>[];
19
72
  layout?: LayoutNode;
20
73
  sessionName?: string;
21
- }, pluginManager: PluginManager): Promise<void>;
74
+ }
75
+ export declare function loadSessionSpec(spec: SessionSpec, pluginManager: PluginManager): Promise<void>;
@@ -1,10 +1,14 @@
1
1
  import { isSessionWithAddAssembly, isSessionWithAddSessionTrack, } from '@jbrowse/core/util';
2
+ import { legacyInitMessage, unknownKeysMessage, } from '@jbrowse/core/util/withLaunchInput';
2
3
  import { isAlive, isStateTreeNode } from '@jbrowse/mobx-state-tree';
3
4
  import { when } from 'mobx';
4
- function isSessionWithWorkspaceLayout(session) {
5
- return ('applyLayoutSpec' in session &&
6
- 'setUseWorkspaces' in session &&
7
- 'orderViews' in session);
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;
8
12
  }
9
13
  function isSessionWithSpecConnections(session) {
10
14
  return ('addConnectionConf' in session &&
@@ -57,37 +61,75 @@ async function whenConnectionsSettle(session) {
57
61
  await when(() => gone() || session.connectionInstances.every(conn => !conn.loading));
58
62
  return !gone();
59
63
  }
60
- // Convert LayoutNode (view indices into the spec's `views` array) to
61
- // LayoutSpecNode (view IDs). `viewIds` is indexed the same as that spec
62
- // array — element i is the id of the view the i-th spec entry created, or
63
- // undefined if it created none — so a layout index maps to the right view
64
- // regardless of what order the views ended up in on the session.
65
- function convertLayoutNode(node, viewIds) {
66
- if (node.views !== undefined) {
67
- // Panel node - convert view indices to view IDs
68
- const ids = node.views
69
- .map(idx => viewIds[idx])
70
- .filter((id) => id !== undefined);
71
- return { viewIds: ids, size: node.size };
72
- }
73
- if (node.children) {
74
- // Container node - recursively convert children
75
- return {
76
- direction: node.direction,
77
- children: node.children.map(child => convertLayoutNode(child, viewIds)),
78
- size: node.size,
79
- };
80
- }
81
- return {};
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
+ ];
82
113
  }
83
114
  // A layout index is a position in the spec's `views` array, so one past the end
84
115
  // (or negative) is an authoring slip that would otherwise silently drop that
85
- // view from the layout, or, if every index in a panel is bad, leave an empty
86
- // panel with no clue why.
87
- function outOfRangeLayoutIndices(node, viewCount) {
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.
88
124
  return [
89
- ...(node.views?.filter(idx => idx < 0 || idx >= viewCount) ?? []),
90
- ...(node.children?.flatMap(child => outOfRangeLayoutIndices(child, viewCount)) ?? []),
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
+ : []),
91
133
  ];
92
134
  }
93
135
  // A `tabs` node shares one cell between its children rather than dividing the
@@ -103,9 +145,9 @@ function outOfRangeLayoutIndices(node, viewCount) {
103
145
  // nested or partial sizes had been ignored while the layout honoured them —
104
146
  // which is also what website/docs/urlparams.md promises.
105
147
  function unsizeableLayoutNodes(layout) {
106
- const sizedTabsChild = layout.direction === 'tabs' &&
107
- (layout.children?.some(c => c.size !== undefined) ?? false);
108
- return (sizedTabsChild || (layout.children?.some(unsizeableLayoutNodes) ?? false));
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);
109
151
  }
110
152
  // The other statement a `tabs` node cannot take literally, and the one that
111
153
  // used to fail silently rather than partially: a tab holds a FLAT stack of
@@ -115,25 +157,179 @@ function unsizeableLayoutNodes(layout) {
115
157
  // to be dropped from the layout, after which homing swept them into whichever
116
158
  // tab happened to be showing, so the arrangement was wrong with nothing said.
117
159
  function flattenedTabsContainers(layout) {
118
- const nestedTabsChild = layout.direction === 'tabs' &&
119
- (layout.children?.some(c => c.children !== undefined) ?? false);
120
- return (nestedTabsChild || (layout.children?.some(flattenedTabsContainers) ?? false));
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 } : {}) };
121
291
  }
122
- // use extension point named e.g. LaunchView-LinearGenomeView to initialize an
123
- // LGV session
124
- export async function loadSessionSpec({ views, sessionAssemblies = [], sessionConnections = [], sessionTracks = [], layout, sessionName, }, pluginManager) {
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;
125
302
  const rootModel = pluginManager.rootModel;
126
303
  try {
127
304
  rootModel.setSession?.({
128
305
  name: sessionName ?? `New session ${new Date().toLocaleString()}`,
129
306
  });
130
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
+ }
131
317
  // Assemblies first: sessionTracks and the views below reference them by
132
318
  // name, so a self-contained spec (novel assemblies + their tracks, no
133
319
  // hosted config) resolves only if the assemblies exist before either runs.
134
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
135
324
  for (const assembly of sessionAssemblies) {
136
- session.addSessionAssembly(assembly);
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
+ }
137
333
  }
138
334
  }
139
335
  // Connections after the assemblies (a connection config may name one the
@@ -180,63 +376,21 @@ export async function loadSessionSpec({ views, sessionAssemblies = [], sessionCo
180
376
  }
181
377
  }
182
378
  addSessionTracks(session, sessionTracks);
183
- // a view type with no registered LaunchView-<type> extension point makes
379
+ // A view type with no registered LaunchView-<type> extension point makes
184
380
  // evaluateAsyncExtensionPoint a silent no-op, leaving an empty session with
185
- // no diagnostic. Two different causes, so two messages: the view type is
186
- // unknown here at all (a typo, or a plugin that wasn't loaded), or it exists
187
- // but nothing taught it how to launch from a spec, which the spec author
188
- // can't fix, only the view's plugin can.
381
+ // no diagnostic. Reported once per type, not per view.
382
+ const viewTypes = pluginManager.getElementTypeRecord('view');
189
383
  const notLaunchable = [
190
- ...new Set(views.flatMap(view => pluginManager.extensionPoints.has(`LaunchView-${view.type}`)
191
- ? []
192
- : [view.type])),
384
+ ...new Set(views.flatMap(view => viewTypeProblem(pluginManager, view.type) ? [view.type] : [])),
193
385
  ];
194
- // the record's `has`, not getElementType, which throws on an unregistered
195
- // name rather than returning undefined
196
- const viewTypes = pluginManager.getElementTypeRecord('view');
197
386
  const unknown = notLaunchable.filter(type => !viewTypes.has(type));
198
387
  const noLauncher = notLaunchable.filter(type => viewTypes.has(type));
199
388
  if (unknown.length) {
200
- session?.notifyError(`Unknown view type(s) in session spec: ${unknown.join(', ')}. The plugin providing the view may be missing, or the type may be misspelled.`);
389
+ session?.notifyError(unknownViewTypesMessage(unknown));
201
390
  }
202
391
  if (noLauncher.length) {
203
- session?.notifyError(`View type(s) ${noLauncher.join(', ')} cannot be launched from a session spec: no LaunchView extension point is registered for them.`);
204
- }
205
- // The two ways of writing a view are not the same shape, and this is the
206
- // confusable pair:
207
- //
208
- // - a spec view is flat *arguments* to LaunchView-<type>, in that launcher's
209
- // own vocabulary (a dotplot takes `views`, a spreadsheet takes `uri`), and
210
- // the launcher sorts them into what the new view needs
211
- // - a config/defaultSession view is MST *state*, so keys that need resolving
212
- // on load have nowhere to live but the view's own `init` property
213
- //
214
- // Reported rather than unwrapped here, deliberately: only a launcher knows
215
- // which of its keys are one-shot commands versus plain view props, so
216
- // flattening `init` centrally would erase that distinction and let a view prop
217
- // nested inside it be honored from a spec while the identical config drops it.
218
- // Teaching each launcher to accept both instead is per-view-type work that
219
- // every future launcher has to remember — the shapes then differ by view type,
220
- // which is worse than differing by surface. One check here covers every view
221
- // type, including plugin-provided ones. Without it the LGV reports the
222
- // downstream symptom, "No assembly provided", for a misplaced block.
223
- const nested = views.filter(view => 'init' in view).map(view => view.type);
224
- if (nested.length) {
225
- session?.notifyError(`Session spec view(s) ${nested.join(', ')} nest their settings under "init". A spec takes those keys flat (assembly, loc, tracks, …); "init" is the config/defaultSession form.`);
392
+ session?.notifyError(noLauncherMessage(noLauncher));
226
393
  }
227
- // Launch sequentially and record the id each spec view created, so the
228
- // layout below can map its indices to real views. Reading session.views
229
- // positionally afterwards only works while every handler happens to addView
230
- // synchronously and in order; capturing the delta per launch instead is
231
- // correct even if a handler awaits or adds an auxiliary view, and lets a
232
- // later spec view (e.g. a connected MsaView) reference an earlier one that
233
- // now already exists. `type` is the dispatch key, not view init data:
234
- // forwarding it would land in the view's declarative init blob and trip the
235
- // spurious "init ignored unknown key(s): type" warning meant to catch typos.
236
- // `displayName` is applied here rather than forwarded because it is a base
237
- // view prop every view type has, so one path covers all of them (including
238
- // plugin-provided types whose launcher never heard of it). A nested `init` is
239
- // NOT unwrapped the same way — see the diagnostic above for why.
240
394
  // Let any connection this spec registered finish before the views that
241
395
  // reference what it supplies are launched (see whenConnectionsSettle). Gated
242
396
  // on there being views: with none, nothing is waiting on the connection, and
@@ -250,54 +404,62 @@ export async function loadSessionSpec({ views, sessionAssemblies = [], sessionCo
250
404
  !(await whenConnectionsSettle(session))) {
251
405
  return;
252
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.
253
420
  const createdViewIds = [];
254
- for (const { type, displayName, ...view } of views) {
255
- const before = new Set(session?.views.map(v => v.id));
256
- // Strict so a launch handler that throws (missing/invalid assembly,
257
- // unresolved track, ...) surfaces as a snackbar instead of being swallowed
258
- // by the plain extension-point runner, which would leave a silent empty
259
- // session. A handler that legitimately no-ops returns normally and is
260
- // unaffected. Per-view try/catch so one bad view doesn't abort the rest.
261
- try {
262
- await pluginManager.evaluateAsyncExtensionPointStrict(`LaunchView-${type}`, {
263
- ...view,
264
- session,
265
- });
266
- }
267
- catch (e) {
268
- console.error(e);
269
- session?.notifyError(`Failed to launch ${type} view: ${e}`, e);
421
+ for (const spec of views) {
422
+ const { view, problem } = await launchableSpecView(pluginManager, spec);
423
+ if (problem) {
424
+ session?.notifyError(problem);
270
425
  }
271
- const created = session?.views.find(v => !before.has(v.id));
272
- if (created && displayName) {
273
- created.setDisplayName(displayName);
426
+ const { created, failure } = await launchSpecView(session, pluginManager, view);
427
+ if (failure) {
428
+ session?.notifyError(failure.message, failure.cause);
274
429
  }
275
- createdViewIds.push(created?.id);
430
+ createdViewIds.push(created.map(v => v.id));
276
431
  }
277
432
  if (layout && session) {
278
- const badIndices = outOfRangeLayoutIndices(layout, views.length);
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');
279
437
  if (badIndices.length) {
280
438
  session.notifyError(`Session spec layout references view index ${badIndices.join(', ')}, but the spec has ${views.length} view(s).`);
281
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
+ }
282
443
  if (unsizeableLayoutNodes(layout)) {
283
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');
284
445
  }
285
446
  if (flattenedTabsContainers(layout)) {
286
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');
287
448
  }
288
- if (isSessionWithWorkspaceLayout(session)) {
289
- // Enable workspaces mode for this session only — a spec URL shouldn't
290
- // rewrite the visitor's own preference
291
- session.setUseWorkspaces(true);
292
- // A tab's `viewIds` is membership, not order: a tab renders its views
293
- // in `session.views` order (WorkspaceContainer's `viewsOf`, and
294
- // `viewIdsForTab`). So the top-to-bottom order a spec panel states —
295
- // `viewIds` is documented as "the views to stack vertically in one tab"
296
- // — only takes effect if it is applied to `session.views`, which is
297
- // what `applyLayoutSpec`'s return value is for. Drop this call and the
298
- // stated order is silently ignored: the tree holds it, nothing reads
299
- // it, and the views come back in launch order with no diagnostic.
300
- session.orderViews(session.applyLayoutSpec(convertLayoutNode(layout, createdViewIds)));
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
+ }
301
463
  }
302
464
  else {
303
465
  session.notifyError('Session spec has a "layout", but this application does not support workspace layouts');