@jbrowse/app-core 5.0.0-beta.2 → 5.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/JbApi/jbApi.d.ts +62 -65
- package/esm/JbApi/jbApi.js +215 -96
- package/esm/SessionSpec/loadSessionSpec.js +5 -19
- package/esm/WorkspaceLayout/model.d.ts +12 -0
- package/esm/WorkspaceLayout/model.js +27 -0
- package/esm/ui/App/AppReadyMarker.d.ts +3 -3
- package/esm/ui/App/AppReadyMarker.js +12 -6
- package/package.json +3 -3
package/esm/JbApi/jbApi.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
|
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 {};
|
package/esm/JbApi/jbApi.js
CHANGED
|
@@ -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
|
|
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
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
?
|
|
286
|
-
|
|
287
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
424
|
-
|
|
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
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
|
|
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
|
|
477
|
-
const sessionId =
|
|
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
|
-
|
|
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
|
-
|
|
526
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
664
|
-
//
|
|
665
|
-
|
|
666
|
-
|
|
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(
|
|
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
|
-
|
|
724
|
-
//
|
|
725
|
-
const
|
|
726
|
-
return
|
|
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
|
|
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
|
|
372
|
-
//
|
|
373
|
-
//
|
|
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.
|
|
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
|
|
23
|
-
*
|
|
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.
|
|
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
|
|
31
|
-
*
|
|
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
|
|
50
|
-
const tracks =
|
|
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.
|
|
3
|
+
"version": "5.0.0-beta.4",
|
|
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/
|
|
33
|
-
"@jbrowse/core": "^5.0.0-beta.
|
|
32
|
+
"@jbrowse/core": "^5.0.0-beta.4",
|
|
33
|
+
"@jbrowse/product-core": "^5.0.0-beta.4"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"react": ">=19.0.0",
|