@jbrowse/app-core 5.0.0-beta.2 → 5.0.0-beta.3

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.
@@ -6,7 +6,7 @@ import * as mst from '@jbrowse/mobx-state-tree';
6
6
  import * as mobx from 'mobx';
7
7
  import type PluginManager from '@jbrowse/core/PluginManager';
8
8
  import type { AnyConfigurationModel } from '@jbrowse/core/configuration';
9
- import type { AbstractSessionModel } from '@jbrowse/core/util/types';
9
+ import type { AbstractSessionModel, AbstractViewModel } from '@jbrowse/core/util/types';
10
10
  import type { IStateTreeNode } from '@jbrowse/mobx-state-tree';
11
11
  /**
12
12
  * What jb.getFeatures may pull before it refuses. An explicit request wins so
@@ -28,7 +28,7 @@ interface TrackSelf extends IStateTreeNode {
28
28
  configuration: AnyConfigurationModel & {
29
29
  trackId: string;
30
30
  };
31
- displays?: (Record<string, unknown> & DisplaySelf)[];
31
+ activeDisplay?: Record<string, unknown> & DisplaySelf;
32
32
  }
33
33
  export declare function sessionOf(pluginManager: PluginManager | undefined): AbstractSessionModel | undefined;
34
34
  export declare function safeJson(value: unknown): string;
@@ -42,7 +42,12 @@ export declare function waitReady(timeoutMs: number, session?: AbstractSessionMo
42
42
  level: string;
43
43
  message: string;
44
44
  }[] | undefined;
45
- notReady?: (({
45
+ notReady?: ({
46
+ error?: string | undefined;
47
+ phase?: string | undefined;
48
+ viewId: string;
49
+ type: string;
50
+ } | ({
46
51
  trackId: string;
47
52
  } & Record<"error", unknown>) | ({
48
53
  trackId: string;
@@ -50,6 +55,7 @@ export declare function waitReady(timeoutMs: number, session?: AbstractSessionMo
50
55
  offscreen?: {
51
56
  pageHeight: number;
52
57
  windowHeight: number;
58
+ scrollY: number;
53
59
  views: {
54
60
  viewId: string;
55
61
  top: number;
@@ -64,7 +70,12 @@ export declare function waitReady(timeoutMs: number, session?: AbstractSessionMo
64
70
  level: string;
65
71
  message: string;
66
72
  }[] | undefined;
67
- notReady?: (({
73
+ notReady?: ({
74
+ error?: string | undefined;
75
+ phase?: string | undefined;
76
+ viewId: string;
77
+ type: string;
78
+ } | ({
68
79
  trackId: string;
69
80
  } & Record<"error", unknown>) | ({
70
81
  trackId: string;
@@ -72,6 +83,7 @@ export declare function waitReady(timeoutMs: number, session?: AbstractSessionMo
72
83
  offscreen?: {
73
84
  pageHeight: number;
74
85
  windowHeight: number;
86
+ scrollY: number;
75
87
  views: {
76
88
  viewId: string;
77
89
  top: number;
@@ -94,6 +106,7 @@ interface SlotDescription {
94
106
  declare function describeSlots(conf: AnyConfigurationModel): Record<string, SlotDescription>;
95
107
  export declare function ensureReExports(): Promise<void>;
96
108
  export declare function createJbApi(pluginManager: PluginManager): {
109
+ help: string;
97
110
  readonly session: AbstractSessionModel;
98
111
  readonly rootModel: import("@jbrowse/core/util").AbstractRootModel | undefined;
99
112
  require: (lib: string | ((...args: any) => any) | {
@@ -111,13 +124,18 @@ export declare function createJbApi(pluginManager: PluginManager): {
111
124
  renameRegionsIfNeeded: typeof renameRegionsIfNeeded;
112
125
  createStopToken: typeof createStopToken;
113
126
  stopStopToken: typeof stopStopToken;
114
- waitReady: (timeoutMs: number) => Promise<{
127
+ waitReady: (timeoutMs?: number) => Promise<{
115
128
  settled: boolean;
116
129
  notifications?: {
117
130
  level: string;
118
131
  message: string;
119
132
  }[] | undefined;
120
- notReady?: (({
133
+ notReady?: ({
134
+ error?: string | undefined;
135
+ phase?: string | undefined;
136
+ viewId: string;
137
+ type: string;
138
+ } | ({
121
139
  trackId: string;
122
140
  } & Record<"error", unknown>) | ({
123
141
  trackId: string;
@@ -125,6 +143,7 @@ export declare function createJbApi(pluginManager: PluginManager): {
125
143
  offscreen?: {
126
144
  pageHeight: number;
127
145
  windowHeight: number;
146
+ scrollY: number;
128
147
  views: {
129
148
  viewId: string;
130
149
  top: number;
@@ -139,7 +158,12 @@ export declare function createJbApi(pluginManager: PluginManager): {
139
158
  level: string;
140
159
  message: string;
141
160
  }[] | undefined;
142
- notReady?: (({
161
+ notReady?: ({
162
+ error?: string | undefined;
163
+ phase?: string | undefined;
164
+ viewId: string;
165
+ type: string;
166
+ } | ({
143
167
  trackId: string;
144
168
  } & Record<"error", unknown>) | ({
145
169
  trackId: string;
@@ -147,6 +171,7 @@ export declare function createJbApi(pluginManager: PluginManager): {
147
171
  offscreen?: {
148
172
  pageHeight: number;
149
173
  windowHeight: number;
174
+ scrollY: number;
150
175
  views: {
151
176
  viewId: string;
152
177
  top: number;
@@ -197,7 +222,8 @@ export declare function createJbApi(pluginManager: PluginManager): {
197
222
  assemblyNames: string[];
198
223
  }[];
199
224
  };
200
- trackModel: (trackId: string) => TrackSelf | undefined;
225
+ view: (viewId?: string) => AbstractViewModel;
226
+ trackModel: (trackId: string, viewId?: string) => TrackSelf;
201
227
  visibleRegions: (viewId?: string) => Promise<JbRegion[]>;
202
228
  loadSessionSpec: (spec: Record<string, unknown>, settleMs?: number) => Promise<{
203
229
  settled: boolean;
@@ -205,7 +231,12 @@ export declare function createJbApi(pluginManager: PluginManager): {
205
231
  level: string;
206
232
  message: string;
207
233
  }[] | undefined;
208
- notReady?: (({
234
+ notReady?: ({
235
+ error?: string | undefined;
236
+ phase?: string | undefined;
237
+ viewId: string;
238
+ type: string;
239
+ } | ({
209
240
  trackId: string;
210
241
  } & Record<"error", unknown>) | ({
211
242
  trackId: string;
@@ -213,6 +244,7 @@ export declare function createJbApi(pluginManager: PluginManager): {
213
244
  offscreen?: {
214
245
  pageHeight: number;
215
246
  windowHeight: number;
247
+ scrollY: number;
216
248
  views: {
217
249
  viewId: string;
218
250
  top: number;
@@ -232,7 +264,12 @@ export declare function createJbApi(pluginManager: PluginManager): {
232
264
  level: string;
233
265
  message: string;
234
266
  }[] | undefined;
235
- notReady?: (({
267
+ notReady?: ({
268
+ error?: string | undefined;
269
+ phase?: string | undefined;
270
+ viewId: string;
271
+ type: string;
272
+ } | ({
236
273
  trackId: string;
237
274
  } & Record<"error", unknown>) | ({
238
275
  trackId: string;
@@ -240,6 +277,7 @@ export declare function createJbApi(pluginManager: PluginManager): {
240
277
  offscreen?: {
241
278
  pageHeight: number;
242
279
  windowHeight: number;
280
+ scrollY: number;
243
281
  views: {
244
282
  viewId: string;
245
283
  top: number;
@@ -253,64 +291,19 @@ export declare function createJbApi(pluginManager: PluginManager): {
253
291
  views: Record<string, unknown>[];
254
292
  } | undefined;
255
293
  }>;
256
- addTrack: (opts: Record<string, unknown>) => Promise<{
257
- trackId: string;
258
- trackType: string;
259
- adapterType: string;
260
- assembly: string;
261
- } | {
262
- settled: boolean;
263
- notifications?: {
264
- level: string;
265
- message: string;
266
- }[] | undefined;
267
- notReady?: (({
268
- trackId: string;
269
- } & Record<"error", unknown>) | ({
270
- trackId: string;
271
- } & Record<"phase", unknown>))[] | undefined;
272
- offscreen?: {
273
- pageHeight: number;
274
- windowHeight: number;
275
- views: {
276
- viewId: string;
277
- top: number;
278
- bottom: number;
279
- }[];
280
- note: string;
281
- } | undefined;
282
- trackId: string;
283
- trackType: string;
284
- adapterType: string;
285
- assembly: string;
286
- shownInView: string;
287
- } | {
288
- settled: boolean;
289
- note: string;
290
- notifications?: {
291
- level: string;
292
- message: string;
293
- }[] | undefined;
294
- notReady?: (({
295
- trackId: string;
296
- } & Record<"error", unknown>) | ({
297
- trackId: string;
298
- } & Record<"phase", unknown>))[] | undefined;
299
- offscreen?: {
300
- pageHeight: number;
301
- windowHeight: number;
302
- views: {
303
- viewId: string;
304
- top: number;
305
- bottom: number;
306
- }[];
307
- note: string;
308
- } | undefined;
294
+ addTrack: (opts: {
295
+ location: string | string[];
296
+ index?: string;
297
+ assembly?: string;
298
+ name?: string;
299
+ show?: boolean;
300
+ viewId?: string;
301
+ settleMs?: number;
302
+ }) => Promise<{
309
303
  trackId: string;
310
304
  trackType: string;
311
305
  adapterType: string;
312
306
  assembly: string;
313
- shownInView: string;
314
307
  }>;
315
308
  getFeatures: (args: string | {
316
309
  trackId: string;
@@ -319,7 +312,11 @@ export declare function createJbApi(pluginManager: PluginManager): {
319
312
  regions?: JbRegion[];
320
313
  viewId?: string;
321
314
  byteLimit?: number;
322
- }, positionalLoc?: string) => Promise<import("@jbrowse/core/util").Feature[]>;
315
+ }, positionalLoc?: string, positionalOpts?: {
316
+ assembly?: string;
317
+ viewId?: string;
318
+ byteLimit?: number;
319
+ }) => Promise<import("@jbrowse/core/util").Feature[]>;
323
320
  };
324
321
  export type JbApi = ReturnType<typeof createJbApi>;
325
322
  export {};
@@ -1,8 +1,11 @@
1
1
  import { setReExportRegistry } from '@jbrowse/core/ReExports/registry';
2
2
  import { getConf, getConfigurationSchemaDefinition, isSlotDefinitionEntry, readConfObject, } from '@jbrowse/core/configuration';
3
+ import { releaseAdapterSession, retainAdapterSession, } from '@jbrowse/core/data_adapters/adapterSessionRefcount';
4
+ import { adapterConfigCacheKey } from '@jbrowse/core/data_adapters/dataAdapterCache';
3
5
  import { getFeatureAdapterOrThrow } from '@jbrowse/core/data_adapters/getFeatureAdapter';
4
- import { adapterByteLimit, measureRegionBytes, } from '@jbrowse/core/rpc/byteBudget';
6
+ import { adapterByteLimit } from '@jbrowse/core/rpc/byteBudget';
5
7
  import { getRpcSessionId, isElectron, isSessionWithAddSessionTrack, objectHash, parseLocString, renameRegionsIfNeeded, } from '@jbrowse/core/util';
8
+ import { openTracks, openViews } from '@jbrowse/core/util/openViews';
6
9
  import { createStopToken, stopStopToken } from '@jbrowse/core/util/stopToken';
7
10
  import { allSessionTracks, getConfAssemblyNamesOrNone, guessTrackConfForLocation, isSameAssemblyName, viewCanDisplayTrack, viewDisplayNames, } from '@jbrowse/core/util/tracks';
8
11
  import * as mst from '@jbrowse/mobx-state-tree';
@@ -28,18 +31,15 @@ export function agentByteLimit(declaredFetchSizeLimit, requested) {
28
31
  function viewSelf(view) {
29
32
  return view;
30
33
  }
31
- // The views' own census contract (BaseViewModel derives `allViews` and
32
- // `ownTracks`), so this module carries no copy of the view nesting: a synteny
33
- // stack's rows and per-band containers, a breakpoint split view's panels, and
34
- // any depth of nesting all arrive through each view answering for itself.
35
- function allViews(session) {
36
- return session.views.flatMap(v => v.allViews);
37
- }
34
+ // The census helpers over the views' own `ownViews`/`ownTracks`, so this module
35
+ // carries no copy of the view nesting: a synteny stack's rows and per-band
36
+ // containers, a breakpoint split view's panels, and any depth of nesting all
37
+ // arrive through each view declaring what it holds.
38
38
  function viewTracks(view) {
39
39
  return view.ownTracks;
40
40
  }
41
41
  function allTracks(session) {
42
- return allViews(session).flatMap(v => viewTracks(v));
42
+ return openTracks(session);
43
43
  }
44
44
  // The drawn display's on-screen state. `displayPhase` is the tree's own
45
45
  // single-sourced vocabulary (render-core/displayPhase.ts) and the same one
@@ -47,7 +47,7 @@ function allTracks(session) {
47
47
  // subtree rather than raising a snackbar, so a settle reporting only
48
48
  // notifications calls them ready.
49
49
  function displayState(track) {
50
- const display = track.displays?.[0];
50
+ const display = track.activeDisplay;
51
51
  if (!display) {
52
52
  return {};
53
53
  }
@@ -72,11 +72,23 @@ function displayState(track) {
72
72
  export function sessionOf(pluginManager) {
73
73
  return pluginManager?.rootModel?.session;
74
74
  }
75
+ // A view whose init failed (its assembly was never found) paints the error in
76
+ // place of a genome and raises no toast, and stays uninitialized: without
77
+ // this, a spec with "volvix" for "volvox" settled false with nothing said.
78
+ function viewState(v) {
79
+ return {
80
+ ...(v.error === undefined ? {} : { error: String(v.error) }),
81
+ ...(v.error === undefined && v.initialized === false
82
+ ? { phase: 'initializing' }
83
+ : {}),
84
+ };
85
+ }
75
86
  function viewSummary(view) {
76
87
  const v = viewSelf(view);
77
88
  return {
78
89
  id: v.id,
79
90
  type: v.type,
91
+ ...viewState(v),
80
92
  ...(v.displayName ? { displayName: v.displayName } : {}),
81
93
  ...(v.assemblyNames?.length ? { assemblyNames: v.assemblyNames } : {}),
82
94
  ...(v.coarseVisibleLocStrings
@@ -225,8 +237,7 @@ const delay = (ms) => new Promise(resolve => {
225
237
  // ~600ms FetchVisibleRegions debounce, or one sample taken right after a
226
238
  // navigation reads the pre-navigation frame as finished. Same reasoning and
227
239
  // same constant as `waitForAppSettled` in products/jbrowse-capture/src/waits.ts
228
- // (APP_SETTLED_HOLD_MS) — reached independently there, and measured; see
229
- // REJECTED_IDEAS under "Waiting out a screenshot action's work".
240
+ // (APP_SETTLED_HOLD_MS) — reached independently there, and measured.
230
241
  const READY_HOLD_MS = 1000;
231
242
  // An error toast stays up until a human dismisses it, and an info toast leaves
232
243
  // on its own — so "the last five messages" repeated one stale error in every
@@ -282,9 +293,18 @@ root = document) {
282
293
  // replaces its own subtree instead of raising a snackbar, so notifications
283
294
  // alone report a clean settle over a browser with a blank track in it.
284
295
  const notReady = session
285
- ? allTracks(session)
286
- .map(t => ({ trackId: t.configuration.trackId, ...displayState(t) }))
287
- .filter(t => 'phase' in t || 'error' in t)
296
+ ? [
297
+ ...openViews(session)
298
+ .map(v => ({
299
+ viewId: v.id,
300
+ type: v.type,
301
+ ...viewState(viewSelf(v)),
302
+ }))
303
+ .filter(v => 'phase' in v || 'error' in v),
304
+ ...allTracks(session)
305
+ .map(t => ({ trackId: t.configuration.trackId, ...displayState(t) }))
306
+ .filter(t => 'phase' in t || 'error' in t),
307
+ ]
288
308
  : [];
289
309
  const offscreen = session ? offscreenViews(session, root) : undefined;
290
310
  return {
@@ -306,7 +326,7 @@ function offscreenViews(session, root) {
306
326
  const containers = new Map([
307
327
  ...root.querySelectorAll('[data-testid^="view-container-"]'),
308
328
  ].map(el => [el.dataset.testid.slice('view-container-'.length), el]));
309
- const views = allViews(session).flatMap(view => {
329
+ const views = openViews(session).flatMap(view => {
310
330
  const rect = containers.get(view.id)?.getBoundingClientRect();
311
331
  return rect && (rect.top < 0 || rect.bottom > windowHeight)
312
332
  ? [
@@ -322,8 +342,9 @@ function offscreenViews(session, root) {
322
342
  ? {
323
343
  pageHeight,
324
344
  windowHeight,
345
+ scrollY: Math.round(win.scrollY),
325
346
  views,
326
- note: 'the session is taller than the window; a viewport screenshot cuts these views off — shrink track heights, or screenshot with fullPage: true',
347
+ note: 'the session is taller than the window; a viewport screenshot cuts these views off — shrink track heights, or screenshot with fullPage: true. A view with a negative top is scrolled out above the viewport (scrollY says by how much), not missing',
327
348
  }
328
349
  : undefined;
329
350
  }
@@ -334,16 +355,24 @@ function trackEntry(conf) {
334
355
  name: readConfObject(conf, 'name'),
335
356
  type: conf.type,
336
357
  ...(adapter.type ? { adapterType: adapter.type } : {}),
337
- assemblyNames: readConfObject(conf, 'assemblyNames'),
358
+ // getConfAssemblyNamesOrNone, not the slot: an assembly's sequence track
359
+ // has no assemblyNames slot and answers through its parent assembly
360
+ assemblyNames: getConfAssemblyNamesOrNone(conf),
338
361
  };
339
362
  }
363
+ // Each assembly's own sequence track, which no track list holds: the config
364
+ // keeps it under the assembly as `sequence`, so `jb.getFeatures` on its
365
+ // trackId reads bases while `listTracks` never named it.
366
+ function sequenceTracks(session) {
367
+ return session.assemblyManager.assemblyList.map(asm => asm.sequence);
368
+ }
340
369
  function listTracks(session, searchArg, limitArg) {
341
370
  const search = searchArg?.toLowerCase() ?? '';
342
371
  const limit = limitArg ?? 100;
343
372
  // allSessionTracks, not session.tracks: connection-supplied tracks (hubs,
344
373
  // registries) are absent from the session lists but fully showable — a
345
374
  // hand-rolled union here hid them from agents entirely
346
- const matches = allSessionTracks(session)
375
+ const matches = [...allSessionTracks(session), ...sequenceTracks(session)]
347
376
  .map(c => trackEntry(c))
348
377
  .filter(t => !search ||
349
378
  t.trackId.toLowerCase().includes(search) ||
@@ -358,7 +387,7 @@ function listTracks(session, searchArg, limitArg) {
358
387
  }
359
388
  function pickView(session, args, capability, wants) {
360
389
  const viewId = typeof args.viewId === 'string' ? args.viewId : '';
361
- const candidates = allViews(session);
390
+ const candidates = openViews(session);
362
391
  if (viewId) {
363
392
  const named = candidates.find(v => v.id === viewId);
364
393
  if (!named) {
@@ -388,18 +417,80 @@ function pickView(session, args, capability, wants) {
388
417
  if (!canDisplay.length) {
389
418
  throw new Error(`No open view can display a ${wants.trackType} (open views: ${onAssembly.map(v => v.type).join(', ')})`);
390
419
  }
391
- return canDisplay[0];
420
+ return onlyView(canDisplay, 'could take this');
421
+ }
422
+ function describeView(view) {
423
+ const v = viewSelf(view);
424
+ const on = v.assemblyNames?.length ? ` on ${v.assemblyNames.join(', ')}` : '';
425
+ const at = v.coarseVisibleLocStrings ? ` at ${v.coarseVisibleLocStrings}` : '';
426
+ return `${v.id} (${v.type}${on}${at})`;
427
+ }
428
+ // A name that matches several views has no right first answer: two linear
429
+ // views of one assembly at two loci both "show the track", and taking the
430
+ // first restyles one row or reads one region while the settle reports both.
431
+ // The named-but-missing case already throws and lists the open views, so the
432
+ // unnamed-and-plural case does the same.
433
+ function onlyView(candidates, relation) {
434
+ const [first, ...rest] = candidates;
435
+ if (!first) {
436
+ throw new Error('No open view');
437
+ }
438
+ if (rest.length) {
439
+ throw new Error(`${candidates.length} views ${relation}: ${candidates.map(v => describeView(v)).join('; ')} — pass viewId to say which`);
440
+ }
441
+ return first;
442
+ }
443
+ function viewById(session, viewId) {
444
+ const candidates = openViews(session);
445
+ if (viewId !== undefined) {
446
+ const named = candidates.find(v => v.id === viewId);
447
+ if (!named) {
448
+ throw new Error(`No view with id "${viewId}". Open views: ${candidates.map(v => describeView(v)).join('; ') || 'none'}`);
449
+ }
450
+ return named;
451
+ }
452
+ if (!candidates.length) {
453
+ throw new Error('No view is open — jb.loadSessionSpec can open one');
454
+ }
455
+ return onlyView(candidates, 'are open');
392
456
  }
393
- function shownTrackModel(session, trackId) {
394
- return allTracks(session).find(t => t.configuration.trackId === trackId);
457
+ function shownTrackModel(session, trackId, viewId) {
458
+ const pool = viewId === undefined
459
+ ? allTracks(session)
460
+ : viewTracks(viewById(session, viewId));
461
+ const shown = pool.filter(t => t.configuration.trackId === trackId);
462
+ if (shown.length > 1) {
463
+ const where = openViews(session)
464
+ .filter(v => viewTracks(v).some(t => t.configuration.trackId === trackId))
465
+ .map(v => describeView(v));
466
+ throw new Error(`"${trackId}" is shown in ${shown.length} views: ${where.join('; ')} — pass viewId to say which`);
467
+ }
468
+ const [track] = shown;
469
+ // undefined here was followed by ".activeDisplay" on the next line of every
470
+ // agent's code, and a TypeError says nothing about which of the two causes
471
+ // it was
472
+ if (!track) {
473
+ throw new Error(session.getTrackById(trackId)
474
+ ? `"${trackId}" is not shown in ${viewId === undefined ? 'any open view' : `view ${viewId}`} — view.showTrack("${trackId}") first; jb.sessionSummary() lists what each view shows`
475
+ : `No track with trackId "${trackId}" — jb.listTracks() shows what is available`);
476
+ }
477
+ return track;
395
478
  }
396
479
  async function locToRegion(session, conf, loc, assemblyArg) {
397
480
  // getConfAssemblyNamesOrNone, not the assemblyNames slot: an assembly's own
398
481
  // sequence track has no such slot and answers through its parent assembly
399
- const assemblyName = assemblyArg ?? getConfAssemblyNamesOrNone(conf)[0];
482
+ const trackAssemblies = getConfAssemblyNamesOrNone(conf);
483
+ const assemblyName = assemblyArg ?? trackAssemblies[0];
400
484
  if (assemblyName === undefined) {
401
485
  throw new Error('The track names no assembly; pass assembly explicitly');
402
486
  }
487
+ // a named assembly the track is not on renames the region against the wrong
488
+ // alias set and answers with the wrong assembly's coordinates, or nothing
489
+ if (assemblyArg !== undefined &&
490
+ trackAssemblies.length &&
491
+ !trackAssemblies.some(name => isSameAssemblyName(name, assemblyArg, session.assemblyManager))) {
492
+ throw new Error(`Track "${conf.trackId}" is on ${trackAssemblies.join(', ')}, not "${assemblyArg}"`);
493
+ }
403
494
  const assembly = await session.assemblyManager.waitForAssembly(assemblyName);
404
495
  if (!assembly) {
405
496
  throw new Error(`Assembly "${assemblyName}" could not be loaded`);
@@ -414,24 +505,37 @@ async function locToRegion(session, conf, loc, assemblyArg) {
414
505
  end: parsed.end ?? bounds?.end ?? Number.MAX_SAFE_INTEGER,
415
506
  };
416
507
  }
417
- async function visibleRegionsOf(session, viewId, preferTrackId) {
508
+ async function visibleRegionsOf(session, viewId, preferTrackId, trackAssemblies = []) {
418
509
  // `in`, not evaluation: visibleRegions is a getter that THROWS ("width
419
510
  // undefined") until the view's component mounts and sets a width — a
420
511
  // freshly spec-loaded view stays in that state briefly even after the
421
512
  // app-phase marker reads ready, since a view with no width has no display
422
513
  // fetching anything.
423
- const candidates = allViews(session).filter(v => (!viewId || v.id === viewId) && 'visibleRegions' in v);
424
- // among region-bearing views, the one actually showing the track wins — two
425
- // views on two assemblies would otherwise send the first view's namespace to
426
- // the second view's file, which answers nothing, silently
427
- const chosen = candidates.find(v => viewTracks(v).some(t => t.configuration.trackId === preferTrackId)) ?? candidates[0];
428
- if (!chosen) {
514
+ const regionBearing = openViews(session).filter(v => (!viewId || v.id === viewId) && 'visibleRegions' in v);
515
+ if (!regionBearing.length) {
429
516
  throw new Error('No view that shows a region — pass loc, or open a linear view first');
430
517
  }
518
+ // a view on another assembly than the track would hand its region to a file
519
+ // that has no such sequence, which answers nothing, silently
520
+ const candidates = trackAssemblies.length
521
+ ? regionBearing.filter(v => !viewSelf(v).assemblyNames?.length ||
522
+ viewSelf(v).assemblyNames.some(name => trackAssemblies.some(t => isSameAssemblyName(name, t, session.assemblyManager))))
523
+ : regionBearing;
524
+ if (!candidates.length) {
525
+ throw new Error(`No open view is on ${trackAssemblies.join(', ')} (open views: ${regionBearing.map(v => describeView(v)).join('; ')}) — pass loc, or open a view on that assembly`);
526
+ }
527
+ // among region-bearing views, the ones actually showing the track — two
528
+ // views on two assemblies would otherwise send the first view's namespace to
529
+ // the second view's file, which answers nothing, silently
530
+ const showing = candidates.filter(v => viewTracks(v).some(t => t.configuration.trackId === preferTrackId));
531
+ const chosen = onlyView(showing.length ? showing : candidates, showing.length ? `show "${preferTrackId}"` : 'show a region');
431
532
  const view = viewSelf(chosen);
432
533
  const deadline = Date.now() + 10_000;
433
534
  let visible;
434
535
  while (visible === undefined) {
536
+ if (view.error !== undefined) {
537
+ throw new Error(`View ${view.id} failed to initialize: ${String(view.error)} — pass loc with assembly, or open a view that loads`);
538
+ }
435
539
  if (view.initialized !== false) {
436
540
  try {
437
541
  visible = view.visibleRegions;
@@ -458,78 +562,61 @@ async function visibleRegionsOf(session, viewId, preferTrackId) {
458
562
  assemblyName,
459
563
  }));
460
564
  }
461
- // Main-thread adapter, not the CoreGetFeatures RPC: the RPC serializes every
462
- // feature in the region across the worker boundary before any limit can
463
- // apply. Here the features stay objects. The shown track's own rpcSessionId,
464
- // so this shares the adapter instance — parsed indexes and chunk caches
465
- // included — that the display already warmed (rpcSessionId lives on track
466
- // models, so the walk cannot start at the session; session.id is the
467
- // cold-namespace fallback for un-shown tracks). Regions are renamed the same
468
- // way the RPC base class renames them: they arrive carrying the assembly's
469
- // canonical refNames, and a file spelling them differently would otherwise
470
- // answer nothing, silently.
471
- async function fetchFeatures(pluginManager, session, trackId, regions, requestedByteLimit) {
565
+ // The same RPCs the track's display issues, on the same worker: the sessionId
566
+ // is `adapterConfigCacheKey` of the adapter config, which is exactly what
567
+ // `BaseTrackModel.rpcSessionId` derives, so a shown track's parsed index and
568
+ // chunk cache are reused rather than rebuilt in a main-thread twin that nothing
569
+ // ever freed. Retained and released like a track does, so an un-shown track's
570
+ // worker cache is dropped once this read is done. Regions are renamed to the
571
+ // file's own refNames during serialization (RpcMethodTypeWithRenameRegions),
572
+ // and the features come back rebuilt as SimpleFeature. What crosses the worker
573
+ // boundary is bounded by the byte gate that runs first.
574
+ async function fetchFeatures(session, trackId, regions, requestedByteLimit) {
472
575
  const conf = session.getTrackById(trackId);
473
576
  if (!conf) {
474
577
  throw new Error(`No track with trackId "${trackId}" — jb.listTracks() shows what is available`);
475
578
  }
476
- const trackModel = shownTrackModel(session, trackId);
477
- const sessionId = trackModel
478
- ? getRpcSessionId(trackModel)
479
- : (session.id ?? 'mcp');
480
- const renamed = await renameRegionsIfNeeded(session.assemblyManager, {
481
- regions,
482
- adapterConfig: readConfObject(conf, 'adapter'),
483
- sessionId,
484
- });
485
- const dataAdapter = await getFeatureAdapterOrThrow({
486
- pluginManager,
487
- sessionId,
488
- adapterConfig: renamed.adapterConfig,
489
- sequenceAdapter: renamed.sequenceAdapter,
490
- });
579
+ const adapterConfig = readConfObject(conf, 'adapter');
580
+ const sessionId = adapterConfigCacheKey(adapterConfig);
491
581
  // The adapter's own declared limit where it has one, so this does not quietly
492
582
  // disagree with the size the track's display already refuses to render —
493
583
  // the reasoning BaseTrackModel.exportByteLimit spells out for "Save track
494
584
  // data", which is the other path that fetches because someone asked rather
495
585
  // than because a view scrolled.
496
586
  const byteLimit = agentByteLimit(readConfObject(conf, ['adapter', 'fetchSizeLimit']), requestedByteLimit);
587
+ const { rpcManager } = session;
497
588
  const stopToken = createStopToken();
498
589
  // desktop's MCP relay gives up at 150s, so the read must not outlive it —
499
590
  // and an agent-triggered read of a dense region wants a ceiling either way
500
591
  const stopTimer = setTimeout(() => {
501
592
  stopStopToken(stopToken);
502
593
  }, 120_000);
503
- // The agent's "region too large". A display refuses to fetch over its own
504
- // gate and paints the reason; this path has no display, so without the same
505
- // question asked here an agent that names a whole chromosome pulls it — over
506
- // someone else's data host, onto the main thread, in the page it is about to
507
- // screenshot. Same index-only estimate the gated RPC takes first, and a
508
- // refusal rather than a truncation: a short answer that looked like the whole
509
- // answer is the failure this surface exists to prevent.
510
- const { tooLarge, bytes } = await measureRegionBytes({
511
- dataAdapter,
512
- regions: renamed.regions,
513
- byteLimit,
514
- stopToken,
515
- });
516
- if (tooLarge) {
517
- clearTimeout(stopTimer);
518
- throw new Error(`region too large for jb.getFeatures: the largest region is ~${bytes} bytes against a limit of ${byteLimit}. Narrow the region, or pass an explicit byteLimit if you mean to pull this much.`);
519
- }
520
- // one array per region, flattened once at the end: a dense region returns
521
- // hundreds of thousands of features, so neither push(...array) — a
522
- // RangeError on the argument list — nor a concat per region is available
523
- const perRegion = [];
594
+ retainAdapterSession(rpcManager, sessionId);
524
595
  try {
525
- for (const region of renamed.regions) {
526
- perRegion.push(await dataAdapter.getFeaturesArray(region, { stopToken }));
596
+ // The agent's "region too large". A display refuses to fetch over its own
597
+ // gate and paints the reason; this path has no display, so without the
598
+ // same question asked here an agent that names a whole chromosome pulls it
599
+ // — over someone else's data host, into the page it is about to
600
+ // screenshot. Same index-only estimate the gated RPC takes first, and a
601
+ // refusal rather than a truncation: a short answer that looked like the
602
+ // whole answer is the failure this surface exists to prevent.
603
+ const bytes = await rpcManager.call(sessionId, 'CoreGetRegionByteEstimate',
604
+ // eslint-disable-next-line no-restricted-syntax -- reports nothing: an agent awaits the returned features and no display or dialog exists to show a phase label or bar
605
+ { adapterConfig, regions, scope: 'largestRegion', stopToken });
606
+ if (bytes !== undefined && bytes > byteLimit) {
607
+ throw new Error(`region too large for jb.getFeatures: the largest region is ~${bytes} bytes against a limit of ${byteLimit}. Narrow the region, or pass an explicit byteLimit if you mean to pull this much.`);
527
608
  }
609
+ // eslint-disable-next-line no-restricted-syntax -- reports nothing: see the estimate above
610
+ return await rpcManager.call(sessionId, 'CoreGetFeatures', {
611
+ adapterConfig,
612
+ regions,
613
+ stopToken,
614
+ });
528
615
  }
529
616
  finally {
530
617
  clearTimeout(stopTimer);
618
+ void releaseAdapterSession(rpcManager, sessionId);
531
619
  }
532
- return perRegion.flat();
533
620
  }
534
621
  // A LocalPathLocation only reads under Electron: in a browser openLocation
535
622
  // throws "can't use local files in the browser" at the FIRST READ, not here, so
@@ -539,6 +626,12 @@ function fileLocation(spec) {
539
626
  if (/^https?:\/\//.test(spec)) {
540
627
  return { uri: spec, locationType: 'UriLocation' };
541
628
  }
629
+ // The app's working directory is not the agent's, and a relative path
630
+ // resolves against the app's: under a packaged app that is "/", so the read
631
+ // fails at the first fetch and reports through the display, not here.
632
+ if (!/^(?:\/|[a-zA-Z]:[\\/]|\\\\)/.test(spec)) {
633
+ throw new Error(`jb.addTrack needs an absolute local path or a URL: "${spec}" is relative and would resolve against the app's working directory, not yours.`);
634
+ }
542
635
  if (!isElectron) {
543
636
  throw new Error(`jb.addTrack needs a URL in a browser, and "${spec}" is a local path. JBrowse Web cannot read local files except through the Add track file picker, which needs a real file chosen by hand.`);
544
637
  }
@@ -586,7 +679,7 @@ function locationsOf(value) {
586
679
  }
587
680
  // Vocabulary introspection: every config slot a live config node's schema
588
681
  // defines, so code never has to guess which settings keys exist — an unknown
589
- // key is otherwise dropped silently, which is this format's known failure mode.
682
+ // key is not an error, only an entry in applyDisplaySettings' `unapplied` list.
590
683
  function describeSlots(conf) {
591
684
  const definition = getConfigurationSchemaDefinition(conf) ?? {};
592
685
  return Object.fromEntries(Object.entries(definition).flatMap(([name, def]) => isSlotDefinitionEntry(def)
@@ -614,6 +707,19 @@ function describeSlots(conf) {
614
707
  export async function ensureReExports() {
615
708
  setReExportRegistry((await import('@jbrowse/core/ReExports/modules')).default);
616
709
  }
710
+ // The front door for an agent that just found `jb` and knows nothing else —
711
+ // jbrowse-web's console banner and meta tag point here, because a browser
712
+ // agent has no MCP docs tool and no initialize instructions to learn the
713
+ // contract from.
714
+ const JB_HELP = `jb drives this JBrowse app programmatically (window.jb in a browser; the same object is the "jb" argument of JBrowse Desktop's run_javascript MCP tool).
715
+
716
+ Orient first: jb.sessionSummary(). Introspect, never guess: jb.listTracks(search?, limit?) answers { total, tracks } with the trackIds; jb.describeSlots(jb.trackModel('someTrackId').activeDisplay.configuration) for the settings keys a display accepts — an unknown settings key is not an error, it lands in applyDisplaySettings' "unapplied" list, so read the report; jb.inspect('views.0') for a live node's getters, actions and modelType.
717
+
718
+ The model is mobx-state-tree: mutate only through actions (raw assignment throws), and write display settings with track.applyDisplaySettings(settings). Build views declaratively with jb.loadSessionSpec({ views: [{ type: 'LinearGenomeView', assembly, loc, tracks: [...] }] }); arrange the views already open into panels with session.layoutViews({ direction: 'horizontal', children: [{ views: [viewId] }, ...] }) — leaves name view ids or indexes into session.views; add data with jb.addTrack({ location }) (an absolute path or a URL); read data with await jb.getFeatures({ trackId, loc?, assembly?, byteLimit? }) (or jb.getFeatures(trackId, loc?, opts?)), which renames refNames ("chr1" vs "1") so the file answers and reads on the worker the track's display uses — raw adapter code must call jb.renameRegionsIfNeeded itself. After changing anything, await jb.waitReady(ms) and read its notifications and notReady lists before trusting the screen.
719
+
720
+ Views nest and several can be open. jb.view(viewId?) is the open view, and jb.view(), jb.trackModel(trackId), jb.visibleRegions() and jb.addTrack throw naming the candidates rather than picking one when more than one view could answer — pass viewId (from jb.sessionSummary()) to say which.
721
+
722
+ Full guide: https://jbrowse.org/jb2/docs/agents_live_model (JBrowse Desktop serves the same guide offline through its MCP docs tool — Help menu, "Connect an AI agent...").`;
617
723
  // The helper library an agent drives the app through. Built from the plugin
618
724
  // manager alone, so one of these serves a whole app rather than one session —
619
725
  // which is what lets jbrowse-web hand the same object to every caller for the
@@ -627,11 +733,12 @@ export function createJbApi(pluginManager) {
627
733
  const live = () => {
628
734
  const current = sessionOf(pluginManager);
629
735
  if (!current) {
630
- throw new Error('No session is open');
736
+ throw new Error('No session is open, so there is nothing for this helper to read. Build one with jb.loadSessionSpec({ views: [...] }), which needs no session of its own, or load a config with the open tool.');
631
737
  }
632
738
  return current;
633
739
  };
634
740
  const jb = {
741
+ help: JB_HELP,
635
742
  get session() {
636
743
  return live();
637
744
  },
@@ -651,19 +758,26 @@ export function createJbApi(pluginManager) {
651
758
  renameRegionsIfNeeded,
652
759
  createStopToken,
653
760
  stopStopToken,
654
- waitReady: (timeoutMs) => waitReady(timeoutMs, live()),
761
+ // a default because an omitted number made the deadline NaN, and a view
762
+ // that never readied then held the call open to the relay's own timeout
763
+ waitReady: (timeoutMs = 30_000) => waitReady(timeoutMs, live()),
655
764
  sessionSummary: () => sessionSummary(live()),
656
765
  inspect: (path, maxInspectBytes) => inspectSession(live(), { path, maxBytes: maxInspectBytes }),
657
766
  listTracks: (search, limit) => listTracks(live(), search, limit),
658
- trackModel: (trackId) => shownTrackModel(live(), trackId),
767
+ view: (viewId) => viewById(live(), viewId),
768
+ trackModel: (trackId, viewId) => shownTrackModel(live(), trackId, viewId),
659
769
  visibleRegions: (viewId) => visibleRegionsOf(live(), viewId),
660
770
  loadSessionSpec: (spec, settleMs) => loadSpec(pluginManager, { spec, settleMs }),
661
771
  addTrack: (opts) => addTrack(pluginManager, live(), opts),
662
772
  // Two of four filmed takes wrote jb.getFeatures('trackId', loc) and lost a
663
- // turn to "No track with trackId undefined" — so the positional form is
664
- // simply accepted alongside the object it was documented as.
665
- getFeatures: async (args, positionalLoc) => {
666
- const fetchArgs = typeof args === 'string' ? { trackId: args, loc: positionalLoc } : args;
773
+ // turn to "No track with trackId undefined", and a third wrote
774
+ // jb.getFeatures('trackId', loc, { assembly }) and had the options
775
+ // silently ignored — so the positional form is accepted alongside the
776
+ // object it was documented as, options third.
777
+ getFeatures: async (args, positionalLoc, positionalOpts) => {
778
+ const fetchArgs = typeof args === 'string'
779
+ ? { trackId: args, loc: positionalLoc, ...positionalOpts }
780
+ : args;
667
781
  const session = live();
668
782
  const conf = session.getTrackById(fetchArgs.trackId);
669
783
  if (!conf) {
@@ -674,19 +788,22 @@ export function createJbApi(pluginManager) {
674
788
  ? [
675
789
  await locToRegion(session, conf, fetchArgs.loc, fetchArgs.assembly),
676
790
  ]
677
- : await visibleRegionsOf(session, fetchArgs.viewId, fetchArgs.trackId));
678
- return fetchFeatures(pluginManager, session, fetchArgs.trackId, regions, fetchArgs.byteLimit);
791
+ : await visibleRegionsOf(session, fetchArgs.viewId, fetchArgs.trackId, getConfAssemblyNamesOrNone(conf)));
792
+ return fetchFeatures(session, fetchArgs.trackId, regions, fetchArgs.byteLimit);
679
793
  },
680
794
  };
681
795
  return jb;
682
796
  }
797
+ // a spec load on a busy machine passed 30s and answered settled:false over
798
+ // views that were fine; the wait exits the moment they are ready
799
+ const ADD_TRACK_SETTLE_MS = 60_000;
683
800
  async function addTrack(pluginManager, session, args) {
684
801
  if (!isSessionWithAddSessionTrack(session)) {
685
802
  throw new Error('This session cannot add tracks');
686
803
  }
687
804
  const location = locationsOf(args.location);
688
805
  if (!location.length) {
689
- throw new Error('jb.addTrack needs a location (local path or URL)');
806
+ throw new Error('jb.addTrack needs a location (an absolute local path or a URL)');
690
807
  }
691
808
  const requested = typeof args.assembly === 'string' ? args.assembly : session.assemblyNames[0];
692
809
  if (requested === undefined) {
@@ -720,10 +837,12 @@ async function addTrack(pluginManager, session, args) {
720
837
  pluginManager,
721
838
  });
722
839
  viewSelf(view).showTrack(conf.trackId);
723
- // a spec load on a busy machine passed 30s and answered settled:false over
724
- // views that were fine; the wait exits the moment they are ready
725
- const settle = await waitReady(60_000, session);
726
- return { ...summary, ...settle, shownInView: view.id };
840
+ const shown = { ...summary, shownInView: view.id };
841
+ // 0 skips the settle, so several adds can share one jb.waitReady
842
+ const settleMs = typeof args.settleMs === 'number' ? args.settleMs : ADD_TRACK_SETTLE_MS;
843
+ return settleMs > 0
844
+ ? { ...shown, ...(await waitReady(settleMs, session)) }
845
+ : shown;
727
846
  }
728
847
  // Under the 45 s one evaluation gets from the Claude in Chrome extension: a
729
848
  // cold hosted config settling for the full minute answered nothing there,
@@ -3,9 +3,7 @@ import { legacyInitMessage, unknownKeysMessage, } from '@jbrowse/core/util/withL
3
3
  import { isAlive, isStateTreeNode } from '@jbrowse/mobx-state-tree';
4
4
  import { when } from 'mobx';
5
5
  function isSessionWithWorkspaceLayout(session) {
6
- return ('applyLayoutSpec' in session &&
7
- 'setUseWorkspaces' in session &&
8
- 'orderViews' in session);
6
+ return 'layoutViews' in session;
9
7
  }
10
8
  function isSessionWithSpecConnections(session) {
11
9
  return ('addConnectionConf' in session &&
@@ -354,26 +352,14 @@ export async function loadSessionSpec({ views, sessionAssemblies = [], sessionCo
354
352
  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');
355
353
  }
356
354
  if (isSessionWithWorkspaceLayout(session)) {
357
- // Enable workspaces mode for this session only — a spec URL shouldn't
358
- // rewrite the visitor's own preference
359
- session.setUseWorkspaces(true);
360
- // A tab's `viewIds` is membership, not order: a tab renders its views
361
- // in `session.views` order (WorkspaceContainer's `viewsOf`, and
362
- // `viewIdsForTab`). So the top-to-bottom order a spec panel states —
363
- // `viewIds` is documented as "the views to stack vertically in one tab"
364
- // — only takes effect if it is applied to `session.views`, which is
365
- // what `applyLayoutSpec`'s return value is for. Drop this call and the
366
- // stated order is silently ignored: the tree holds it, nothing reads
367
- // it, and the views come back in launch order with no diagnostic.
368
355
  // Its own try/catch, the same reasoning as the per-view one above: the
369
356
  // resolver throws for a layout it will not arrange, and this is the
370
357
  // LAST thing the spec does — so an unarrangeable layout used to take
371
- // the whole load's catch, after `setUseWorkspaces(true)` had already
372
- // run, and report itself as the session's error with the views left
373
- // unordered. The layout is one statement in the spec; losing it should
374
- // cost the spec its layout and nothing else.
358
+ // the whole load's catch and report itself as the session's error. The
359
+ // layout is one statement in the spec; losing it should cost the spec
360
+ // its layout and nothing else.
375
361
  try {
376
- session.orderViews(session.applyLayoutSpec(convertLayoutNode(layout, createdViewIds, knownIds, new Set(pinnedLayoutIds(layout, knownIds)))));
362
+ session.layoutViews(convertLayoutNode(layout, createdViewIds, knownIds, new Set(pinnedLayoutIds(layout, knownIds))));
377
363
  }
378
364
  catch (e) {
379
365
  console.error(e);
@@ -241,6 +241,18 @@ export declare function WorkspaceLayoutMixin(): import("@jbrowse/mobx-state-tree
241
241
  * session spec's layout there is nothing for `orderViews` to apply.
242
242
  */
243
243
  tileViews(mode: TileMode, allViewIds: string[]): void;
244
+ /**
245
+ * #action
246
+ * Arrange the session's views into panels: `applyLayoutSpec` plus the
247
+ * two things it does not do on its own, each of which fails silently
248
+ * without it — turning workspaces mode on for this session (a layout
249
+ * renders nowhere else) and ordering `session.views` to the spec's
250
+ * top-to-bottom statement, which is where a tab reads its order from.
251
+ * Leaves take view ids or indexes into `session.views`; a layout that
252
+ * seats no view throws rather than leaving a blank tab. What a session
253
+ * spec's `layout` and an agent's live re-layout both call.
254
+ */
255
+ layoutViews(spec: LayoutSpecNode): string[];
244
256
  }, import("@jbrowse/mobx-state-tree")._NotCustomized, import("@jbrowse/mobx-state-tree")._NotCustomized>;
245
257
  export type WorkspaceLayoutMixinType = ReturnType<typeof WorkspaceLayoutMixin>;
246
258
  export interface WorkspaceLayout extends Instance<WorkspaceLayoutMixinType> {
@@ -5,6 +5,9 @@ import { activeTabIn, addTab, addViewToTab, findTab, homeViews, moveTabToPanel,
5
5
  function sessionViewIds(self) {
6
6
  return self.views?.map(v => v.id);
7
7
  }
8
+ function layoutHost(self) {
9
+ return self;
10
+ }
8
11
  /**
9
12
  * A caller-supplied view list narrowed to the views the session actually has.
10
13
  *
@@ -555,6 +558,30 @@ export function WorkspaceLayoutMixin() {
555
558
  tileViews(mode, allViewIds) {
556
559
  self.applyLayoutSpec(tileLayoutSpec(liveViewIds(self, allViewIds), mode));
557
560
  },
561
+ /**
562
+ * #action
563
+ * Arrange the session's views into panels: `applyLayoutSpec` plus the
564
+ * two things it does not do on its own, each of which fails silently
565
+ * without it — turning workspaces mode on for this session (a layout
566
+ * renders nowhere else) and ordering `session.views` to the spec's
567
+ * top-to-bottom statement, which is where a tab reads its order from.
568
+ * Leaves take view ids or indexes into `session.views`; a layout that
569
+ * seats no view throws rather than leaving a blank tab. What a session
570
+ * spec's `layout` and an agent's live re-layout both call.
571
+ */
572
+ layoutViews(spec) {
573
+ const host = layoutHost(self);
574
+ if (!host.setUseWorkspaces || !host.orderViews) {
575
+ throw new Error('This session has no view list to arrange: layoutViews needs a host composed with MultipleViewsSessionMixin');
576
+ }
577
+ host.setUseWorkspaces(true);
578
+ const ids = self.applyLayoutSpec(spec);
579
+ if (ids.length === 0) {
580
+ throw new Error('The layout seats no views: a leaf names its views with "views" (view ids, or indexes into session.views) and a container nests "children"');
581
+ }
582
+ host.orderViews(ids);
583
+ return ids;
584
+ },
558
585
  })));
559
586
  }
560
587
  export function isSessionWithWorkspaceLayout(session) {
@@ -19,9 +19,9 @@ import type { AppSession } from './types.ts';
19
19
  * same way the phase publishes whether it is done, so an outside reader asking
20
20
  * "is the track I requested actually open" reads one element instead of
21
21
  * walking `window.JBrowseSession` with its own copy of the view nesting. The
22
- * walk itself is the views' own `allViews`/`ownTracks` contract (BaseViewModel
23
- * derives it), so this component knows nothing about which property a
24
- * container view keeps its children on either.
22
+ * walk itself is `openViews`/`openTracks` over each view's declared
23
+ * `ownViews`/`ownTracks`, so this component knows nothing about which property
24
+ * a container view keeps its children on either.
25
25
  *
26
26
  * Its own component, and `hidden`, for two reasons: an observer here subscribes
27
27
  * to every view's phase without re-rendering the app shell around it, and a
@@ -1,11 +1,17 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { openTracks, openViews } from '@jbrowse/core/util/openViews';
2
3
  import { observer } from 'mobx-react';
3
4
  function trackLoading(track) {
4
5
  return track.displays.some(d => d.displayPhase === 'loading');
5
6
  }
7
+ // A view whose init failed (its assembly was never found) stays uninitialized
8
+ // for good and paints its error, so like a display's terminal phase it is
9
+ // finished, not pending: without this the whole app read `loading` for as long
10
+ // as that view was open, and every settle answered false with no reason.
6
11
  function viewLoading(view) {
7
12
  const flags = view;
8
- return flags.showLoading === true || flags.initialized === false;
13
+ return (flags.error === undefined &&
14
+ (flags.showLoading === true || flags.initialized === false));
9
15
  }
10
16
  /**
11
17
  * One element that says whether the whole app has finished, and what is open.
@@ -27,9 +33,9 @@ function viewLoading(view) {
27
33
  * same way the phase publishes whether it is done, so an outside reader asking
28
34
  * "is the track I requested actually open" reads one element instead of
29
35
  * walking `window.JBrowseSession` with its own copy of the view nesting. The
30
- * walk itself is the views' own `allViews`/`ownTracks` contract (BaseViewModel
31
- * derives it), so this component knows nothing about which property a
32
- * container view keeps its children on either.
36
+ * walk itself is `openViews`/`openTracks` over each view's declared
37
+ * `ownViews`/`ownTracks`, so this component knows nothing about which property
38
+ * a container view keeps its children on either.
33
39
  *
34
40
  * Its own component, and `hidden`, for two reasons: an observer here subscribes
35
41
  * to every view's phase without re-rendering the app shell around it, and a
@@ -46,8 +52,8 @@ function viewLoading(view) {
46
52
  * and says why an error separates them). Both harnesses do it in that order.
47
53
  */
48
54
  const AppReadyMarker = observer(function AppReadyMarker({ session, }) {
49
- const views = session.views.flatMap(v => v.allViews);
50
- const tracks = views.flatMap(v => v.ownTracks);
55
+ const views = openViews(session);
56
+ const tracks = openTracks(session);
51
57
  const loading = views.some(viewLoading) || tracks.some(trackLoading);
52
58
  return (_jsx("span", { hidden: true, "data-testid": "app-ready-marker", "data-app-phase": loading ? 'loading' : 'ready', "data-app-views": session.views.length, "data-app-assemblies": JSON.stringify([
53
59
  ...new Set(views.flatMap(v => v.assemblyNames ?? [])),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/app-core",
3
- "version": "5.0.0-beta.2",
3
+ "version": "5.0.0-beta.3",
4
4
  "type": "module",
5
5
  "description": "JBrowse 2 code shared between the 'full featured' apps e.g. jbrowse-web and jbrowse-desktop",
6
6
  "keywords": [
@@ -29,8 +29,8 @@
29
29
  "@mui/material": "^9.4.0",
30
30
  "mobx": "^7.0.3",
31
31
  "mobx-react": "^10.0.2",
32
- "@jbrowse/product-core": "^5.0.0-beta.2",
33
- "@jbrowse/core": "^5.0.0-beta.2"
32
+ "@jbrowse/core": "^5.0.0-beta.3",
33
+ "@jbrowse/product-core": "^5.0.0-beta.3"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "react": ">=19.0.0",