@jbrowse/capture 5.0.0-beta.1 → 5.0.0-beta.11

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.
@@ -1,126 +1,59 @@
1
1
  import type { Page } from 'puppeteer';
2
2
  export interface SessionExpectations {
3
- /** Assembly that must be open on some view. Usually the one you navigated to. */
3
+ /** Assembly that must be open on some view. */
4
4
  assembly?: string;
5
5
  /**
6
- * trackIds that must all be open. The exact ids, not a count: a hosted config
7
- * usually ships a defaultSession, so `&tracks=` ADDS to tracks that are
8
- * already there and a count is satisfied before your track arrives — or by
9
- * the default set alone when the id you passed does not exist at all.
6
+ * trackIds that must all be open. Exact ids rather than a count, since a
7
+ * config's default session may already hold others.
10
8
  */
11
9
  trackIds?: string[];
10
+ /** The fewest open views that count. Default 1; 0 accepts none. */
11
+ views?: number;
12
12
  }
13
- export interface SessionSummary {
13
+ export interface SessionCensus {
14
14
  views: number;
15
15
  assemblies: string[];
16
16
  trackIds: string[];
17
17
  }
18
- /** What the page currently has open, or undefined if there is no session yet. */
19
- export declare function readSessionSummary(page: Page): Promise<SessionSummary | undefined>;
20
- export declare function readSessionSummaryInPage(): SessionSummary | undefined;
21
18
  /**
22
- * Wait until the session exists, every view reports itself initialized, and the
23
- * assembly and tracks that were asked for are actually open.
24
- *
25
- * Throws on timeout rather than proceeding. A config URL that 404s, a trackId
26
- * that does not exist in the config, and an assembly name that does not match
27
- * the one the config declares all fail here, which is the only place they fail
28
- * at all — each of them otherwise produces a browser that loads, paints its
29
- * chrome, and photographs beautifully with nothing in it.
19
+ * The load failure the app shows in place of itself, the census
20
+ * `AppReadyMarker` publishes beside its phase, and any snackbar up — which is
21
+ * where a location that did not resolve says so. Serialized into the page.
30
22
  */
31
- export declare function waitForSession(page: Page, { assembly, trackIds, timeout, }?: SessionExpectations & {
32
- timeout?: number;
33
- }): Promise<void>;
34
- export declare const PAINT_CONTRACT_NOTE: string;
23
+ export declare function readSessionInPage(): {
24
+ failure?: string;
25
+ census?: SessionCensus;
26
+ notices: string[];
27
+ };
35
28
  /**
36
- * Whether every open display's paint state was actually measurable.
37
- *
38
- * The per-display paint attributes are not universal. Measured on 2026-08-07:
39
- * jbrowse.org/code/jb2/main publishes `data-view-phase`, `data-display-phase`
40
- * and `data-display-drawn`; jbrowse.org/code/jb2/latest — the released build,
41
- * which is what every genomes.jbrowse.org link and every docs figure link opens
42
- * — publishes none of them and exposes only the loading overlay. Against that
43
- * instance the display-level waits are unfalsifiable rather than satisfied, so a
44
- * caller has to be told which of the two it got instead of reading "0 displays
45
- * pending" as good news.
46
- *
47
- * The absence of the attribute is NOT on its own the answer, which is the trap
48
- * here: a page with no tracks open — an import form, a menu shot, a bare view —
49
- * publishes none of it either, and reporting that as "this build cannot measure
50
- * paint" is both wrong and alarming. So this asks the question only when there
51
- * is something to measure, and answers true when there is not.
52
- *
53
- * Call it AFTER the session gate, so "no tracks open" means the page genuinely
54
- * has none rather than not having got there yet.
29
+ * Wait until the census shows the assembly and tracks that were asked for.
30
+ * Throws at once with the app's message if it could not load, and on timeout
31
+ * with what the census held instead.
55
32
  */
56
- export declare function hasPaintContract(page: Page): Promise<boolean>;
57
- /**
58
- * Which readiness attributes this build publishes at all.
59
- *
60
- * `hasPaintContract` answers the same question for one attribute and folds in
61
- * "there was nothing to measure"; this is the raw read of all three, so the
62
- * chain can adapt to the build BEFORE it starts waiting rather than reporting
63
- * afterwards what it could not see.
64
- *
65
- * Every wait keyed on one of these is NEGATIVE — it passes when the selector is
66
- * absent — so on a build that publishes none of them all three are satisfied by
67
- * a page that has not begun to draw. Read this after the session gate and treat
68
- * `false` as "this signal is unavailable", never as "this signal says done".
69
- */
70
- export interface Instrumentation {
71
- viewPhase: boolean;
72
- displayPhase: boolean;
73
- displayDrawn: boolean;
74
- }
75
- /** Serialized into the page; exported so a test can call the real function. */
76
- export declare function readInstrumentationInPage(): Instrumentation;
77
- export declare function readInstrumentation(page: Page): Promise<Instrumentation>;
78
- /**
79
- * Displays that were still reporting unpainted at the moment of the call.
80
- *
81
- * Distinct from `unsettled`, which says a wait ran out of time. This says what
82
- * the page looked like when the shutter fired, and the two do not imply each
83
- * other: a display can go back to pending after its stage passed. An empty
84
- * result means nothing at all on a build with no paint contract — check
85
- * `paintContract` before reading it as good news.
86
- */
87
- export declare function pendingDisplays(page: Page): Promise<string[]>;
88
- /** One unpainted display and what it says about itself. */
89
- export interface PendingDisplay {
90
- /** the display TYPE, shared by every instance of it */
33
+ export declare function waitForSession(page: Page, { assembly, trackIds, views, timeout, }?: SessionExpectations & {
34
+ timeout?: number;
35
+ }): Promise<void>;
36
+ /** One display and the phase it publishes. */
37
+ export interface DisplayState {
38
+ /** The display type, or its id where a banner has replaced the display. */
91
39
  name: string;
92
- /** which instance, where the display publishes `data-display-id` */
93
40
  id?: string;
94
- /**
95
- * Its own phase, or undefined on a build too old to publish one. `loading` is
96
- * a display still fetching; `ready` is the one that earns this census, since
97
- * a display that says it has finished while reporting no paint is a bug in
98
- * the display rather than a slow page.
99
- */
100
41
  phase?: string;
101
42
  }
102
43
  /**
103
- * The same census, with each display's own account of itself.
104
- *
105
- * `pendingDisplays` above answers WHICH, and that was the whole report: every
106
- * timeout read the same however it was caused. The phase separates the cases a
107
- * bare name runs together —
108
- *
109
- * `loading` still fetching. A slow page or a fetch that never returns.
110
- * `error` finished, badly. Nothing is coming; the picture is a banner.
111
- * `ready` it says it is done and reports no paint. That is the display's
112
- * bug, not the wait's, and it is the one a longer timeout will
113
- * never fix.
114
- * absent the element publishes no phase: a build older than the
115
- * attribute, or a surface that never grew one.
116
- *
117
- * Serialized into the page, so a test can call the real function. Read fresh
118
- * from the DOM at report time rather than from handles the waits held. A handle to an element that has since re-rendered throws
119
- * `Node is detached from document`, which is how the previous attempt at this
120
- * turned four diagnosable timeouts into nine opaque puppeteer errors (reverted
121
- * in 28c6ee6d90).
44
+ * Displays that are not showing their data. `pending`: unpainted, canceled,
45
+ * or showing an error, each failing a capture — `loading` is still fetching,
46
+ * `error` and `renderError` show a banner, `canceled` lasts until Retry, and
47
+ * `ready` is a display that says it finished without painting. `tooLarge`:
48
+ * showing "too much data", which is the app working as designed. Serialized
49
+ * into the page.
50
+ *
51
+ * `error` is selected by phase as well as by paint: an LGV display that failed
52
+ * its fetch counts as painted, so its banner publishes `drawn="true"`.
122
53
  */
123
- export declare function pendingDisplayStatesInPage(): PendingDisplay[];
124
- export declare function pendingDisplayStates(page: Page): Promise<PendingDisplay[]>;
125
- /** `pendingDisplayStates` as one line for an error message. */
126
- export declare function describePendingDisplays(pending: PendingDisplay[]): string;
54
+ export declare function displayCensusInPage(): {
55
+ pending: DisplayState[];
56
+ tooLarge: DisplayState[];
57
+ };
58
+ /** Displays as one clause for a message. */
59
+ export declare function describeDisplays(displays: DisplayState[]): string;
@@ -1,196 +1,102 @@
1
- /** What the page currently has open, or undefined if there is no session yet. */
2
- export function readSessionSummary(page) {
3
- return page.evaluate(readSessionSummaryInPage);
4
- }
5
- // Serialized into the page, so it can only call what it declares — hence the
6
- // inlined copy of readViews rather than a shared import.
7
- //
8
- // Exported for its test: it reads `globalThis.JBrowseSession` and nothing else,
9
- // so calling it in node against a stubbed global exercises the very function
10
- // puppeteer serializes, rather than a copy of it that can drift.
11
- export function readSessionSummaryInPage() {
12
- const session = globalThis.JBrowseSession;
13
- const views = session?.views;
14
- if (!views) {
15
- return undefined;
16
- }
17
- // Inlined rather than shared with the gate below for the same reason as the
18
- // readViews copy: both are serialized into the page.
19
- function collect(v) {
20
- return [
21
- ...(v.tracks ?? []),
22
- ...(v.levels ?? []).flatMap(l => l.tracks ?? []),
23
- ...(v.views ?? []).flatMap(collect),
24
- ];
25
- }
26
- return {
27
- views: views.length,
28
- assemblies: [
29
- ...new Set(views.flatMap(function asm(v) {
30
- return [...(v.assemblyNames ?? []), ...(v.views ?? []).flatMap(asm)];
31
- })),
32
- ],
33
- trackIds: views.flatMap(v => collect(v).map(t => t.configuration?.trackId ?? '(unnamed)')),
34
- };
35
- }
1
+ import { DEFAULT_TIMEOUT, holdTrue } from './poll.js';
36
2
  /**
37
- * Wait until the session exists, every view reports itself initialized, and the
38
- * assembly and tracks that were asked for are actually open.
39
- *
40
- * Throws on timeout rather than proceeding. A config URL that 404s, a trackId
41
- * that does not exist in the config, and an assembly name that does not match
42
- * the one the config declares all fail here, which is the only place they fail
43
- * at all — each of them otherwise produces a browser that loads, paints its
44
- * chrome, and photographs beautifully with nothing in it.
3
+ * The load failure the app shows in place of itself, the census
4
+ * `AppReadyMarker` publishes beside its phase, and any snackbar up — which is
5
+ * where a location that did not resolve says so. Serialized into the page.
45
6
  */
46
- export async function waitForSession(page, { assembly, trackIds = [], timeout = 60000, } = {}) {
7
+ export function readSessionInPage() {
8
+ const failure = document.querySelector('[data-app-error]')?.dataset.appError;
9
+ const notices = [
10
+ ...document.querySelectorAll('[data-testid^="snackbar-"]'),
11
+ ].map(el => el.textContent.trim());
12
+ const marker = document.querySelector('[data-app-tracks]');
13
+ if (!marker) {
14
+ return { failure, notices };
15
+ }
47
16
  try {
48
- // #region session-gate
49
- await page.waitForFunction((wantAssembly, wantTracks) => {
50
- const session = globalThis.JBrowseSession;
51
- const views = session?.views;
52
- if (!views?.length) {
53
- return false;
54
- }
55
- // `initialized` is an LGV getter; a view type without one is mounted
56
- // content the moment it exists, so absent counts as initialized and
57
- // only an explicit false is pending.
58
- if (views.some(v => v.initialized === false)) {
59
- return false;
60
- }
61
- // A container view (synteny, dotplot) keeps its assemblies on the rows
62
- // and its tracks on the levels, so both walks descend into sub-views
63
- // and levels rather than reading the top view only.
64
- const asmOf = (v) => [
65
- ...(v.assemblyNames ?? []),
66
- ...(v.views ?? []).flatMap(asmOf),
67
- ];
68
- if (wantAssembly !== null &&
69
- !views.some(v => asmOf(v).includes(wantAssembly))) {
70
- return false;
71
- }
72
- const tracksOf = (v) => [
73
- ...(v.tracks ?? []),
74
- ...(v.levels ?? []).flatMap(l => l.tracks ?? []),
75
- ...(v.views ?? []).flatMap(tracksOf),
76
- ];
77
- const open = new Set(views.flatMap(v => tracksOf(v).map(t => t.configuration?.trackId)));
78
- return wantTracks.every(id => open.has(id));
79
- }, { timeout, polling: 250 }, assembly ?? null, trackIds);
80
- // #endregion session-gate
17
+ return {
18
+ failure,
19
+ notices,
20
+ census: {
21
+ views: Number(marker.dataset.appViews) || 0,
22
+ assemblies: JSON.parse(marker.dataset.appAssemblies ?? '[]'),
23
+ trackIds: JSON.parse(marker.dataset.appTracks ?? '[]'),
24
+ },
25
+ };
81
26
  }
82
27
  catch {
83
- const summary = await readSessionSummary(page);
84
- const found = summary
85
- ? `${summary.views} view(s), assemblies [${summary.assemblies.join(', ')}], tracks [${summary.trackIds.join(', ')}]`
86
- : 'no session on the page at all (is this a jbrowse-web instance?)';
87
- const missing = summary
88
- ? trackIds.filter(id => !summary.trackIds.includes(id))
89
- : trackIds;
28
+ return { failure, notices };
29
+ }
30
+ }
31
+ /**
32
+ * Wait until the census shows the assembly and tracks that were asked for.
33
+ * Throws at once with the app's message if it could not load, and on timeout
34
+ * with what the census held instead.
35
+ */
36
+ export async function waitForSession(page, { assembly, trackIds = [], views = 1, timeout = DEFAULT_TIMEOUT, } = {}) {
37
+ let last;
38
+ const notices = new Set();
39
+ const reached = await holdTrue(async () => {
40
+ const read = await page.evaluate(readSessionInPage).catch(() => undefined);
41
+ if (read?.failure) {
42
+ throw new Error(`JBrowse could not load: ${read.failure}`);
43
+ }
44
+ for (const notice of read?.notices ?? []) {
45
+ notices.add(notice);
46
+ }
47
+ const census = read?.census;
48
+ last = census;
49
+ return (!!census &&
50
+ census.views >= views &&
51
+ (assembly === undefined || census.assemblies.includes(assembly)) &&
52
+ trackIds.every(id => census.trackIds.includes(id)));
53
+ }, { holdMs: 0, timeout, pollMs: 250 });
54
+ if (!reached) {
55
+ const census = last;
56
+ const found = census
57
+ ? `${census.views} view(s), assemblies [${census.assemblies.join(', ')}], tracks [${census.trackIds.join(', ')}]`
58
+ : 'no census on the page at all, so either this is not a JBrowse app or ' +
59
+ 'it is older than v5, the first release to publish one';
60
+ const missing = trackIds.filter(id => !census?.trackIds.includes(id));
90
61
  const wanted = [
91
62
  assembly ? `assembly "${assembly}"` : undefined,
92
63
  missing.length ? `track(s) [${missing.join(', ')}]` : undefined,
93
64
  ]
94
65
  .filter(Boolean)
95
66
  .join(' and ');
96
- throw new Error(`the session never reached the requested state after ${timeout}ms. ` +
97
- `Wanted ${wanted || 'an initialized view'}; found ${found}. ` +
98
- 'A config URL that 404s, a trackId the config does not define, or an ' +
99
- 'assembly name that does not match the config all look like this.');
67
+ const said = notices.size
68
+ ? `The app said: ${[...notices].join(' | ')}.`
69
+ : 'A trackId the config does not define, or an assembly name that does not match the config, looks like this.';
70
+ throw new Error(`the session never reached the requested state after ${timeout}ms. Wanted ${wanted || (views > 0 ? 'an open view' : 'a session')}; found ${found}. ${said}`);
100
71
  }
101
72
  }
102
- export const PAINT_CONTRACT_NOTE = 'this JBrowse build publishes no data-display-drawn attributes, so "every ' +
103
- 'display has painted" cannot be checked — only that nothing is still ' +
104
- 'loading. Raise --settle if the image looks half-drawn.';
105
73
  /**
106
- * Whether every open display's paint state was actually measurable.
74
+ * Displays that are not showing their data. `pending`: unpainted, canceled,
75
+ * or showing an error, each failing a capture — `loading` is still fetching,
76
+ * `error` and `renderError` show a banner, `canceled` lasts until Retry, and
77
+ * `ready` is a display that says it finished without painting. `tooLarge`:
78
+ * showing "too much data", which is the app working as designed. Serialized
79
+ * into the page.
107
80
  *
108
- * The per-display paint attributes are not universal. Measured on 2026-08-07:
109
- * jbrowse.org/code/jb2/main publishes `data-view-phase`, `data-display-phase`
110
- * and `data-display-drawn`; jbrowse.org/code/jb2/latest — the released build,
111
- * which is what every genomes.jbrowse.org link and every docs figure link opens
112
- * — publishes none of them and exposes only the loading overlay. Against that
113
- * instance the display-level waits are unfalsifiable rather than satisfied, so a
114
- * caller has to be told which of the two it got instead of reading "0 displays
115
- * pending" as good news.
116
- *
117
- * The absence of the attribute is NOT on its own the answer, which is the trap
118
- * here: a page with no tracks open — an import form, a menu shot, a bare view —
119
- * publishes none of it either, and reporting that as "this build cannot measure
120
- * paint" is both wrong and alarming. So this asks the question only when there
121
- * is something to measure, and answers true when there is not.
122
- *
123
- * Call it AFTER the session gate, so "no tracks open" means the page genuinely
124
- * has none rather than not having got there yet.
81
+ * `error` is selected by phase as well as by paint: an LGV display that failed
82
+ * its fetch counts as painted, so its banner publishes `drawn="true"`.
125
83
  */
126
- export async function hasPaintContract(page) {
127
- const summary = await readSessionSummary(page);
128
- if (summary && summary.trackIds.length === 0) {
129
- return true;
84
+ export function displayCensusInPage() {
85
+ const pending = [];
86
+ const tooLarge = [];
87
+ for (const el of document.querySelectorAll('[data-display-drawn="false"], [data-display-phase="error"], [data-display-phase="canceled"], [data-display-phase="renderError"], [data-display-phase="tooLarge"]')) {
88
+ const phase = el.dataset.displayPhase;
89
+ (phase === 'tooLarge' ? tooLarge : pending).push({
90
+ name: el.dataset.testid ?? el.dataset.displayId ?? 'unnamed display',
91
+ id: el.dataset.displayId,
92
+ phase,
93
+ });
130
94
  }
131
- return page.evaluate(() => document.querySelector('[data-display-drawn]') !== null);
132
- }
133
- /** Serialized into the page; exported so a test can call the real function. */
134
- export function readInstrumentationInPage() {
135
- return {
136
- viewPhase: document.querySelector('[data-view-phase]') !== null,
137
- displayPhase: document.querySelector('[data-display-phase]') !== null,
138
- displayDrawn: document.querySelector('[data-display-drawn]') !== null,
139
- };
140
- }
141
- export function readInstrumentation(page) {
142
- return page.evaluate(readInstrumentationInPage);
143
- }
144
- /**
145
- * Displays that were still reporting unpainted at the moment of the call.
146
- *
147
- * Distinct from `unsettled`, which says a wait ran out of time. This says what
148
- * the page looked like when the shutter fired, and the two do not imply each
149
- * other: a display can go back to pending after its stage passed. An empty
150
- * result means nothing at all on a build with no paint contract — check
151
- * `paintContract` before reading it as good news.
152
- */
153
- export function pendingDisplays(page) {
154
- return page.evaluate(() => [
155
- ...document.querySelectorAll('[data-display-drawn="false"]'),
156
- ].map(el => el.dataset.testid ?? (el.id || 'unnamed display')));
157
- }
158
- /**
159
- * The same census, with each display's own account of itself.
160
- *
161
- * `pendingDisplays` above answers WHICH, and that was the whole report: every
162
- * timeout read the same however it was caused. The phase separates the cases a
163
- * bare name runs together —
164
- *
165
- * `loading` still fetching. A slow page or a fetch that never returns.
166
- * `error` finished, badly. Nothing is coming; the picture is a banner.
167
- * `ready` it says it is done and reports no paint. That is the display's
168
- * bug, not the wait's, and it is the one a longer timeout will
169
- * never fix.
170
- * absent the element publishes no phase: a build older than the
171
- * attribute, or a surface that never grew one.
172
- *
173
- * Serialized into the page, so a test can call the real function. Read fresh
174
- * from the DOM at report time rather than from handles the waits held. A handle to an element that has since re-rendered throws
175
- * `Node is detached from document`, which is how the previous attempt at this
176
- * turned four diagnosable timeouts into nine opaque puppeteer errors (reverted
177
- * in 28c6ee6d90).
178
- */
179
- export function pendingDisplayStatesInPage() {
180
- return [
181
- ...document.querySelectorAll('[data-display-drawn="false"]'),
182
- ].map(el => ({
183
- name: el.dataset.testid ?? (el.id || 'unnamed display'),
184
- id: el.dataset.displayId,
185
- phase: el.dataset.displayPhase,
186
- }));
187
- }
188
- export function pendingDisplayStates(page) {
189
- return page.evaluate(pendingDisplayStatesInPage);
95
+ return { pending, tooLarge };
190
96
  }
191
- /** `pendingDisplayStates` as one line for an error message. */
192
- export function describePendingDisplays(pending) {
193
- return pending
194
- .map(d => `${d.name}${d.id ? ` (${d.id})` : ''} is ${d.phase ?? 'in an unpublished phase'}`)
97
+ /** Displays as one clause for a message. */
98
+ export function describeDisplays(displays) {
99
+ return displays
100
+ .map(d => `${d.name}${d.id && d.id !== d.name ? ` (${d.id})` : ''} is ${d.phase ?? 'in an unpublished phase'}`)
195
101
  .join('; ');
196
102
  }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * How many CSS pixels of the session lie below the window: the most any
3
+ * scrolling column holding a view overflows, less the empty room `ViewStack`
4
+ * leaves below the last view. Serialized into the page.
5
+ */
6
+ export declare function sessionOverflowInPage(): number;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * How many CSS pixels of the session lie below the window: the most any
3
+ * scrolling column holding a view overflows, less the empty room `ViewStack`
4
+ * leaves below the last view. Serialized into the page.
5
+ */
6
+ export function sessionOverflowInPage() {
7
+ let most = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
8
+ for (const container of document.querySelectorAll('[data-testid^="view-container-"]')) {
9
+ for (let el = container.parentElement; el; el = el.parentElement) {
10
+ const { overflowY } = getComputedStyle(el);
11
+ if ((overflowY === 'auto' || overflowY === 'scroll') &&
12
+ el.scrollHeight > el.clientHeight) {
13
+ const room = el
14
+ .querySelector('[data-testid="view-stack-overscroll"]')
15
+ ?.getBoundingClientRect().height ?? 0;
16
+ most = Math.max(most, Math.ceil(el.scrollHeight - room - el.clientHeight));
17
+ break;
18
+ }
19
+ }
20
+ }
21
+ return most;
22
+ }
package/esm/url.d.ts CHANGED
@@ -1,4 +1,9 @@
1
- export declare const PUBLIC_INSTANCE = "https://jbrowse.org/code/jb2/latest/";
1
+ export declare const PUBLIC_INSTANCE = "https://jbrowse.org/code/jb2/v5.0.0-beta.11/";
2
+ /**
3
+ * The instance as the page that loads it sees it: a directory given without
4
+ * its slash, which the host redirects, resolves relative URLs one level up.
5
+ */
6
+ export declare function instanceUrl(instance?: string): URL;
2
7
  export interface JBrowseUrlOptions {
3
8
  /** A genomes.jbrowse.org assembly: a UCSC db name (`hg38`) or GenArk accession. */
4
9
  hub?: string;
@@ -7,14 +12,19 @@ export interface JBrowseUrlOptions {
7
12
  /** Assembly to open. Defaults to `hub`, whose config names its assembly after it. */
8
13
  assembly?: string;
9
14
  /**
10
- * Where to open. A locstring (`chr1:1,000-2,000`, several space-separated for
11
- * a discontinuous view), or — on a config that ships a text index, which every
12
- * UCSC assembly on genomes.jbrowse.org does — a feature name such as `BRCA1`.
15
+ * Where to open: a locstring (`chr1:1,000-2,000`, several space-separated
16
+ * for a discontinuous view), or a feature name such as `BRCA1` on a config
17
+ * with a text index, which every UCSC assembly on genomes.jbrowse.org has.
13
18
  */
14
19
  loc?: string;
15
20
  /** trackIds from the config to open. */
16
21
  tracks?: string[];
17
- /** The full session spec, for anything the three fields above cannot say. */
22
+ /** A session spec, for anything the three fields above cannot say. */
23
+ spec?: object;
24
+ /**
25
+ * A session saved with "Export session...": the `{ session: {...} }`
26
+ * document, or the snapshot inside it.
27
+ */
18
28
  session?: object;
19
29
  /** Name the session carries once opened. */
20
30
  sessionName?: string;
@@ -22,18 +32,19 @@ export interface JBrowseUrlOptions {
22
32
  instance?: string;
23
33
  }
24
34
  /**
25
- * The URL that opens a JBrowse Web instance onto a described view. Useful on its
26
- * own: an agent can hand this to a human, or paste it into a report, without
27
- * launching a browser at all.
35
+ * A spec or a saved session says which assembly, locations and tracks to
36
+ * open, so the URL carries one of the three ways of saying so, never two.
37
+ */
38
+ export declare function assertSessionStandsAlone({ spec, session, assembly, loc, tracks, }: JBrowseUrlOptions): void;
39
+ /**
40
+ * The URL that opens a JBrowse Web instance onto a described view, for a
41
+ * person to follow without launching a browser here.
28
42
  *
29
- * Two ways to say where to go, and they are not interchangeable. `assembly` /
30
- * `loc` / `tracks` become the [URL parameters](https://jbrowse.org/jb2/docs/urlparams/),
31
- * which route `loc` through the config's text-search index, so a **gene name**
32
- * works there. `session` becomes a
43
+ * `assembly`/`loc`/`tracks` become the
44
+ * [URL parameters](https://jbrowse.org/jb2/docs/urlparams/), whose `loc`
45
+ * takes a gene name. `spec` becomes a
33
46
  * [session spec](https://jbrowse.org/jb2/docs/urlparams/#session-spec), which
34
- * can describe several views and per-display settings but whose `init.loc` is
35
- * parsed as a locstring only and throws on a gene name. Pass one or the other;
36
- * `session` wins, and the URL params are dropped rather than silently starting
37
- * the fresh session that `&loc=` otherwise forces.
47
+ * can describe several views and per-display settings but whose `init.loc`
48
+ * takes a locstring only. `session` opens a saved session as exported.
38
49
  */
39
- export declare function jbrowseUrl({ hub, config, assembly, loc, tracks, session, sessionName, instance, }: JBrowseUrlOptions): string;
50
+ export declare function jbrowseUrl({ hub, config, assembly, loc, tracks, spec, session, sessionName, instance, }: JBrowseUrlOptions): string;
package/esm/url.js CHANGED
@@ -1,44 +1,68 @@
1
1
  import { hubUrl } from './hub.js';
2
- import { sessionSpecParam } from './session.js';
3
- // The public JBrowse Web build the docs' own figure links point at. Any other
4
- // deployment works the same way, including a local `npx serve` of a build.
5
- export const PUBLIC_INSTANCE = 'https://jbrowse.org/code/jb2/latest/';
2
+ import { savedSessionParam, sessionSpecParam } from './session.js';
3
+ import { version } from './version.js';
4
+ // the release this package shipped with; `latest/` trails a major behind
5
+ // during a beta
6
+ export const PUBLIC_INSTANCE = `https://jbrowse.org/code/jb2/v${version}/`;
6
7
  /**
7
- * The URL that opens a JBrowse Web instance onto a described view. Useful on its
8
- * own: an agent can hand this to a human, or paste it into a report, without
9
- * launching a browser at all.
8
+ * The instance as the page that loads it sees it: a directory given without
9
+ * its slash, which the host redirects, resolves relative URLs one level up.
10
+ */
11
+ export function instanceUrl(instance = PUBLIC_INSTANCE) {
12
+ const url = new URL(instance);
13
+ if (!url.pathname.endsWith('/') && !/\.html?$/.test(url.pathname)) {
14
+ url.pathname += '/';
15
+ }
16
+ return url;
17
+ }
18
+ /**
19
+ * A spec or a saved session says which assembly, locations and tracks to
20
+ * open, so the URL carries one of the three ways of saying so, never two.
21
+ */
22
+ export function assertSessionStandsAlone({ spec, session, assembly, loc, tracks, }) {
23
+ if (spec && session) {
24
+ throw new Error('pass a session spec or a saved session, not both');
25
+ }
26
+ if ((spec || session) && (assembly || loc || tracks?.length)) {
27
+ throw new Error('a session spec or saved session says which assembly, locations and ' +
28
+ 'tracks to open, so it cannot be combined with assembly, loc or tracks ' +
29
+ '(--assembly, --loc, --track): put them in the session');
30
+ }
31
+ }
32
+ /**
33
+ * The URL that opens a JBrowse Web instance onto a described view, for a
34
+ * person to follow without launching a browser here.
10
35
  *
11
- * Two ways to say where to go, and they are not interchangeable. `assembly` /
12
- * `loc` / `tracks` become the [URL parameters](https://jbrowse.org/jb2/docs/urlparams/),
13
- * which route `loc` through the config's text-search index, so a **gene name**
14
- * works there. `session` becomes a
36
+ * `assembly`/`loc`/`tracks` become the
37
+ * [URL parameters](https://jbrowse.org/jb2/docs/urlparams/), whose `loc`
38
+ * takes a gene name. `spec` becomes a
15
39
  * [session spec](https://jbrowse.org/jb2/docs/urlparams/#session-spec), which
16
- * can describe several views and per-display settings but whose `init.loc` is
17
- * parsed as a locstring only and throws on a gene name. Pass one or the other;
18
- * `session` wins, and the URL params are dropped rather than silently starting
19
- * the fresh session that `&loc=` otherwise forces.
40
+ * can describe several views and per-display settings but whose `init.loc`
41
+ * takes a locstring only. `session` opens a saved session as exported.
20
42
  */
21
- export function jbrowseUrl({ hub, config, assembly = hub, loc, tracks, session, sessionName, instance = PUBLIC_INSTANCE, }) {
22
- const configUrl = config ?? (hub ? hubUrl(hub) : undefined);
23
- const url = new URL(instance);
24
- // URL's own encoder, not a hand-built query string: a config URL contains
25
- // `://` and `?`, and a session spec is a whole JSON document, so appending
26
- // either raw produces a link that loads a truncated config and reports
27
- // nothing.
43
+ export function jbrowseUrl({ hub, config, assembly, loc, tracks, spec, session, sessionName, instance, }) {
44
+ assertSessionStandsAlone({ spec, session, assembly, loc, tracks });
45
+ const params = new URLSearchParams();
28
46
  const set = (key, value) => {
29
47
  if (value) {
30
- url.searchParams.set(key, value);
48
+ params.set(key, value);
31
49
  }
32
50
  };
33
- set('config', configUrl);
34
- if (session) {
35
- set('session', sessionSpecParam(session));
51
+ set('config', config ?? (hub ? hubUrl(hub) : undefined));
52
+ if (spec) {
53
+ set('session', sessionSpecParam(spec));
54
+ }
55
+ else if (session) {
56
+ set('session', savedSessionParam(session));
36
57
  }
37
58
  else {
38
- set('assembly', assembly);
59
+ set('assembly', assembly ?? hub);
39
60
  set('loc', loc);
40
61
  set('tracks', tracks?.join(','));
41
62
  }
42
63
  set('sessionName', sessionName);
64
+ const url = instanceUrl(instance);
65
+ // the fragment never reaches a server, so a large spec cannot hit HTTP 414
66
+ url.hash = params.toString();
43
67
  return url.href;
44
68
  }