@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.
package/esm/hub.d.ts CHANGED
@@ -1,23 +1,24 @@
1
1
  export declare function hubUrl(hub: string): string;
2
- interface HubTrack {
2
+ export interface Track {
3
3
  trackId: string;
4
4
  type?: string;
5
- name?: string;
5
+ name?: unknown;
6
6
  }
7
- interface HubConfig {
8
- tracks?: HubTrack[];
9
- [key: string]: unknown;
7
+ export declare const trackName: (track: Track) => string;
8
+ /** Tracks whose id or display name contains `filter`, ignoring case. */
9
+ export declare function tracksMatching(tracks: Track[], filter: string): Track[];
10
+ /** Fetch JSON with a timeout, throwing a message that names `what` and the URL. */
11
+ export declare function fetchJson(url: string, what: string): Promise<unknown>;
12
+ interface HostedAssembly {
13
+ name: string;
14
+ organism?: string;
15
+ description?: string;
10
16
  }
11
17
  /**
12
- * Fetch a hosted assembly config from genomes.jbrowse.org by UCSC database name
13
- * or GenArk accession. Use it to discover which trackIds an assembly publishes
14
- * before naming them in a session.
18
+ * The UCSC assemblies genomes.jbrowse.org hosts. Its thousands of GenArk
19
+ * accessions are not listed.
15
20
  */
16
- export declare function fetchHubConfig(hub: string): Promise<HubConfig>;
17
- /**
18
- * The trackIds a hosted assembly publishes, optionally narrowed to those whose
19
- * id or display name contains `filter`. Case-insensitive, because a hub carries
20
- * hundreds of tracks and an agent picking one from a name is the common case.
21
- */
22
- export declare function listHubTracks(hub: string, filter?: string): Promise<HubTrack[]>;
21
+ export declare function listHubAssemblies(): Promise<HostedAssembly[]>;
22
+ /** The tracks a hosted assembly publishes, optionally narrowed by `filter`. */
23
+ export declare function listHubTracks(hub: string, filter?: string): Promise<Track[]>;
23
24
  export {};
package/esm/hub.js CHANGED
@@ -1,43 +1,50 @@
1
1
  const HUB_HOST = 'https://jbrowse.org';
2
- // The genomes.jbrowse.org URL scheme: a UCSC database name (hg38, mm10, ...)
3
- // maps to /ucsc/<db>/config.json; a GenArk accession (GCA_/GCF_...) fans its 9
4
- // digits out into a 3-level directory tree.
5
- //
6
- // Deliberately a copy of `hubUrl` in packages/core/src/util/fetchHub.ts rather
7
- // than an import of it. This package is what an agent reaches for with a bare
8
- // `npx`, and its only runtime dependency is puppeteer; pulling in @jbrowse/core
9
- // (react, mobx, the whole state tree) to evaluate one regex would be the largest
10
- // thing in the install by far. `hub.test.ts` imports both and asserts they agree
11
- // on every shape, so the copy cannot drift silently.
2
+ const FETCH_TIMEOUT_MS = 30000;
3
+ const BROWSE_HINT = 'See https://genomes.jbrowse.org for the available assemblies.';
4
+ // A copy of hubUrl in packages/core/src/util/fetchHub.ts, so an npx install
5
+ // stays puppeteer-only; hub.test.ts holds the two together.
12
6
  export function hubUrl(hub) {
13
7
  const genark = /^(GC[AF])_(\d{3})(\d{3})(\d{3})/.exec(hub);
14
8
  return genark
15
9
  ? `${HUB_HOST}/hubs/genark/${genark[1]}/${genark[2]}/${genark[3]}/${genark[4]}/${hub}/config.json`
16
10
  : `${HUB_HOST}/ucsc/${hub}/config.json`;
17
11
  }
18
- /**
19
- * Fetch a hosted assembly config from genomes.jbrowse.org by UCSC database name
20
- * or GenArk accession. Use it to discover which trackIds an assembly publishes
21
- * before naming them in a session.
22
- */
23
- export async function fetchHubConfig(hub) {
24
- const url = hubUrl(hub);
25
- const res = await fetch(url);
12
+ export const trackName = (track) => typeof track.name === 'string' ? track.name : '';
13
+ /** Tracks whose id or display name contains `filter`, ignoring case. */
14
+ export function tracksMatching(tracks, filter) {
15
+ const needle = filter.toLowerCase();
16
+ return tracks.filter(t => `${t.trackId} ${trackName(t)}`.toLowerCase().includes(needle));
17
+ }
18
+ /** Fetch JSON with a timeout, throwing a message that names `what` and the URL. */
19
+ export async function fetchJson(url, what) {
20
+ const res = await fetch(url, {
21
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
22
+ }).catch((error) => {
23
+ const timedOut = error instanceof Error && error.name === 'TimeoutError';
24
+ throw new Error(`${what} could not be fetched from ${url} ` +
25
+ `(${error instanceof Error ? error.message : error}${timedOut ? `, after ${FETCH_TIMEOUT_MS}ms` : ''}). ${BROWSE_HINT}`);
26
+ });
26
27
  if (!res.ok) {
27
- throw new Error(`hub "${hub}" not found (HTTP ${res.status} from ${url}). ` +
28
- 'See https://genomes.jbrowse.org for the available assemblies.');
28
+ throw new Error(`${what} not found (HTTP ${res.status} from ${url}). ${BROWSE_HINT}`);
29
29
  }
30
- return (await res.json());
30
+ return res.json();
31
31
  }
32
32
  /**
33
- * The trackIds a hosted assembly publishes, optionally narrowed to those whose
34
- * id or display name contains `filter`. Case-insensitive, because a hub carries
35
- * hundreds of tracks and an agent picking one from a name is the common case.
33
+ * The UCSC assemblies genomes.jbrowse.org hosts. Its thousands of GenArk
34
+ * accessions are not listed.
36
35
  */
36
+ export async function listHubAssemblies() {
37
+ const { ucscGenomes = {} } = (await fetchJson(`${HUB_HOST}/ucsc/list.json`, 'the hosted assembly list'));
38
+ return Object.entries(ucscGenomes)
39
+ .map(([name, { organism, description }]) => ({
40
+ name,
41
+ organism,
42
+ description,
43
+ }))
44
+ .sort((a, b) => a.name.localeCompare(b.name));
45
+ }
46
+ /** The tracks a hosted assembly publishes, optionally narrowed by `filter`. */
37
47
  export async function listHubTracks(hub, filter) {
38
- const { tracks = [] } = await fetchHubConfig(hub);
39
- const needle = filter?.toLowerCase();
40
- return needle
41
- ? tracks.filter(t => `${t.trackId} ${t.name ?? ''}`.toLowerCase().includes(needle))
42
- : tracks;
48
+ const { tracks = [] } = (await fetchJson(hubUrl(hub), `hub "${hub}"`));
49
+ return filter ? tracksMatching(tracks, filter) : tracks;
43
50
  }
@@ -0,0 +1 @@
1
+ export declare function assertImagePath(out: string): void;
@@ -0,0 +1,8 @@
1
+ // The types puppeteer infers from a path; it writes a PNG under any other name.
2
+ const IMAGE_EXTENSION = /\.(png|jpe?g|webp)$/i;
3
+ export function assertImagePath(out) {
4
+ if (!IMAGE_EXTENSION.test(out)) {
5
+ throw new Error(`--out needs a .png, .jpg, .jpeg or .webp path, got "${out}" ` +
6
+ '(for SVG, @jbrowse/img renders one without a browser)');
7
+ }
8
+ }
package/esm/index.d.ts CHANGED
@@ -1,10 +1,13 @@
1
- export { captureJBrowse, openJBrowse, waitForJBrowseReady } from './capture.ts';
2
- export { PAINT_CONTRACT_NOTE, describePendingDisplays, hasPaintContract, pendingDisplayStates, pendingDisplayStatesInPage, pendingDisplays, readInstrumentation, readSessionSummary, waitForSession, } from './sessionGate.ts';
3
- export { PUBLIC_INSTANCE, jbrowseUrl } from './url.ts';
4
- export { fetchHubConfig, hubUrl, listHubTracks } from './hub.ts';
5
- export { assemblyFromSession, encodeSessionSpec, sessionSpecParam, sessionSpecQuery, trackIdsFromSession, } from './session.ts';
6
- export { APP_READY, BUSY_SELECTOR, LOADING_OVERLAY, PENDING_DISPLAYS, delay, describePendingDisplaysNow, displayById, displayPainted, displaySettled, hasAppReadyMarker, isPageBusyInPage, waitForAppReady, waitForAppSettled, waitForDisplayPhases, waitForDisplaysDone, waitForLoadingComplete, waitForQuiescent, waitForQuietPeriod, waitForSelectorAttributed, waitForViewPhases, } from './waits.ts';
7
- export { BASE_CHROME_ARGS, SANDBOX_CHROME_ARGS, findChromeExecutable, isBrowserConsoleNoise, } from './browser.ts';
8
- export type { CaptureOptions, CaptureResult, OpenOptions, OpenResult, ReadyOptions, ReadyReport, } from './capture.ts';
9
- export type { Instrumentation, PendingDisplay, SessionExpectations, SessionSummary, } from './sessionGate.ts';
1
+ export { captureJBrowse, openJBrowse } from './capture.ts';
2
+ export { delay } from './poll.ts';
3
+ export { waitForFrame, waitForJBrowseReady } from './ready.ts';
4
+ export { describeDisplays, displayCensusInPage, waitForSession, } from './sessionGate.ts';
5
+ export { jbrowseUrl } from './url.ts';
6
+ export { assemblyFromSession, encodeSessionSpec, sessionSpecQuery, trackIdsFromSession, } from './session.ts';
7
+ export { PENDING_DISPLAYS, displayPainted, displaySettled, waitForAppSettled, waitForSelectorAttributed, } from './waits.ts';
8
+ export { BASE_CHROME_ARGS, findChromeExecutable, isBrowserConsoleNoise, launchBrowser, } from './browser.ts';
9
+ export type { LaunchOptions } from './browser.ts';
10
+ export type { CaptureOptions, CaptureResult, OpenOptions, OpenResult, } from './capture.ts';
11
+ export type { ReadyOptions, ReadyReport } from './ready.ts';
12
+ export type { DisplayState, SessionExpectations } from './sessionGate.ts';
10
13
  export type { JBrowseUrlOptions } from './url.ts';
package/esm/index.js CHANGED
@@ -1,7 +1,8 @@
1
- export { captureJBrowse, openJBrowse, waitForJBrowseReady } from './capture.js';
2
- export { PAINT_CONTRACT_NOTE, describePendingDisplays, hasPaintContract, pendingDisplayStates, pendingDisplayStatesInPage, pendingDisplays, readInstrumentation, readSessionSummary, waitForSession, } from './sessionGate.js';
3
- export { PUBLIC_INSTANCE, jbrowseUrl } from './url.js';
4
- export { fetchHubConfig, hubUrl, listHubTracks } from './hub.js';
5
- export { assemblyFromSession, encodeSessionSpec, sessionSpecParam, sessionSpecQuery, trackIdsFromSession, } from './session.js';
6
- export { APP_READY, BUSY_SELECTOR, LOADING_OVERLAY, PENDING_DISPLAYS, delay, describePendingDisplaysNow, displayById, displayPainted, displaySettled, hasAppReadyMarker, isPageBusyInPage, waitForAppReady, waitForAppSettled, waitForDisplayPhases, waitForDisplaysDone, waitForLoadingComplete, waitForQuiescent, waitForQuietPeriod, waitForSelectorAttributed, waitForViewPhases, } from './waits.js';
7
- export { BASE_CHROME_ARGS, SANDBOX_CHROME_ARGS, findChromeExecutable, isBrowserConsoleNoise, } from './browser.js';
1
+ export { captureJBrowse, openJBrowse } from './capture.js';
2
+ export { delay } from './poll.js';
3
+ export { waitForFrame, waitForJBrowseReady } from './ready.js';
4
+ export { describeDisplays, displayCensusInPage, waitForSession, } from './sessionGate.js';
5
+ export { jbrowseUrl } from './url.js';
6
+ export { assemblyFromSession, encodeSessionSpec, sessionSpecQuery, trackIdsFromSession, } from './session.js';
7
+ export { PENDING_DISPLAYS, displayPainted, displaySettled, waitForAppSettled, waitForSelectorAttributed, } from './waits.js';
8
+ export { BASE_CHROME_ARGS, findChromeExecutable, isBrowserConsoleNoise, launchBrowser, } from './browser.js';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Refuse a JBrowse Web instance older than v5, read off its `version.txt`. An
3
+ * instance that serves no version passes, and the session gate judges it.
4
+ */
5
+ export declare function assertSupportedInstance(instance: string): Promise<void>;
@@ -0,0 +1,21 @@
1
+ import { instanceUrl } from './url.js';
2
+ // the first release to publish `[data-app-phase]` and its census
3
+ const MIN_MAJOR = 5;
4
+ /**
5
+ * Refuse a JBrowse Web instance older than v5, read off its `version.txt`. An
6
+ * instance that serves no version passes, and the session gate judges it.
7
+ */
8
+ export async function assertSupportedInstance(instance) {
9
+ const text = await fetch(new URL('version.txt', instanceUrl(instance)), {
10
+ signal: AbortSignal.timeout(10000),
11
+ })
12
+ .then(res => (res.ok ? res.text() : ''))
13
+ .catch(() => '');
14
+ const version = text.trim();
15
+ const major = /^(\d+)\.\d+\.\d+/.exec(version)?.[1];
16
+ if (major !== undefined && Number(major) < MIN_MAJOR) {
17
+ throw new Error(`${instance} serves JBrowse ${version}, and @jbrowse/capture needs v${MIN_MAJOR} or later: ` +
18
+ 'older builds publish no readiness marker to wait on. Point --instance at a ' +
19
+ `v${MIN_MAJOR} build.`);
20
+ }
21
+ }
package/esm/poll.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ /** The budget each stage of the ready chain gets unless told otherwise. */
2
+ export declare const DEFAULT_TIMEOUT = 60000;
3
+ export declare const delay: (ms: number) => Promise<void>;
4
+ /**
5
+ * Poll `read` until it has answered true for `holdMs` without a break, or
6
+ * `timeout` runs out. A `read` that throws ends the wait with its error.
7
+ * Polled from Node because Chrome throttles in-page timers in a background
8
+ * tab, which is what a headless page is.
9
+ */
10
+ export declare function holdTrue(read: () => Promise<boolean>, { holdMs, timeout, pollMs, }: {
11
+ holdMs: number;
12
+ timeout: number;
13
+ pollMs: number;
14
+ }): Promise<boolean>;
package/esm/poll.js ADDED
@@ -0,0 +1,30 @@
1
+ /** The budget each stage of the ready chain gets unless told otherwise. */
2
+ export const DEFAULT_TIMEOUT = 60000;
3
+ export const delay = (ms) => new Promise(resolve => {
4
+ setTimeout(resolve, ms);
5
+ });
6
+ /**
7
+ * Poll `read` until it has answered true for `holdMs` without a break, or
8
+ * `timeout` runs out. A `read` that throws ends the wait with its error.
9
+ * Polled from Node because Chrome throttles in-page timers in a background
10
+ * tab, which is what a headless page is.
11
+ */
12
+ export async function holdTrue(read, { holdMs, timeout, pollMs, }) {
13
+ const deadline = Date.now() + timeout;
14
+ let since;
15
+ while (Date.now() < deadline) {
16
+ const ok = await read();
17
+ const now = Date.now();
18
+ if (ok) {
19
+ since ??= now;
20
+ if (now - since >= holdMs) {
21
+ return true;
22
+ }
23
+ }
24
+ else {
25
+ since = undefined;
26
+ }
27
+ await delay(pollMs);
28
+ }
29
+ return false;
30
+ }
package/esm/ready.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import type { DisplayState, SessionExpectations } from './sessionGate.ts';
2
+ import type { Page } from 'puppeteer';
3
+ export interface ReadyOptions extends SessionExpectations {
4
+ /** Budget for each wait stage. */
5
+ timeout?: number;
6
+ /**
7
+ * Return the report instead of throwing when a stage times out or a display
8
+ * is not showing its data.
9
+ */
10
+ allowUnsettled?: boolean;
11
+ }
12
+ export interface ReadyReport {
13
+ /** Displays unpainted, canceled or showing an error, each with its phase. */
14
+ pending: DisplayState[];
15
+ /** Displays showing "too much data" in place of their features. Not a failure. */
16
+ tooLarge: DisplayState[];
17
+ /** What did not settle. Empty unless `allowUnsettled`. */
18
+ unsettled: string[];
19
+ }
20
+ /**
21
+ * Wait until the session holds the assembly and tracks asked for, the app has
22
+ * held itself ready, and every display has painted.
23
+ */
24
+ export declare function waitForJBrowseReady(page: Page, options?: ReadyOptions): Promise<ReadyReport>;
25
+ /**
26
+ * `waitForJBrowseReady` without the session gate, for a frame that changed
27
+ * while the session did not, such as after a viewport resize.
28
+ */
29
+ export declare function waitForFrame(page: Page, { timeout, allowUnsettled, }?: Omit<ReadyOptions, keyof SessionExpectations>): Promise<ReadyReport>;
package/esm/ready.js ADDED
@@ -0,0 +1,44 @@
1
+ import { DEFAULT_TIMEOUT } from './poll.js';
2
+ import { describeDisplays, displayCensusInPage, waitForSession, } from './sessionGate.js';
3
+ import { appSettledBlocker } from './waits.js';
4
+ /**
5
+ * Wait until the session holds the assembly and tracks asked for, the app has
6
+ * held itself ready, and every display has painted.
7
+ */
8
+ export async function waitForJBrowseReady(page, options = {}) {
9
+ await waitForSession(page, options);
10
+ return waitForFrame(page, options);
11
+ }
12
+ /**
13
+ * `waitForJBrowseReady` without the session gate, for a frame that changed
14
+ * while the session did not, such as after a viewport resize.
15
+ */
16
+ export async function waitForFrame(page, { timeout = DEFAULT_TIMEOUT, allowUnsettled = false, } = {}) {
17
+ const unsettled = [];
18
+ const blocker = await appSettledBlocker(page, { timeout });
19
+ if (blocker) {
20
+ unsettled.push(`the app never held itself ready (last blocked by ${blocker})`);
21
+ }
22
+ const timedOut = unsettled.length > 0;
23
+ const { pending, tooLarge } = await page.evaluate(displayCensusInPage);
24
+ const failed = pending.filter(d => d.phase === 'error' || d.phase === 'renderError');
25
+ const canceled = pending.filter(d => d.phase === 'canceled');
26
+ const unpainted = pending.filter(d => !failed.includes(d) && !canceled.includes(d));
27
+ if (failed.length > 0) {
28
+ unsettled.push(`display(s) showing an error: ${describeDisplays(failed)}`);
29
+ }
30
+ if (unpainted.length > 0) {
31
+ unsettled.push(`display(s) never painted: ${describeDisplays(unpainted)}`);
32
+ }
33
+ if (canceled.length > 0) {
34
+ unsettled.push(`display(s) canceled, which lasts until Retry: ${describeDisplays(canceled)}`);
35
+ }
36
+ if (!allowUnsettled && unsettled.length > 0) {
37
+ const stillLoading = pending.some(d => d.phase === undefined || d.phase === 'loading');
38
+ const advice = timedOut || stillLoading
39
+ ? 'Raise the timeout, or pass'
40
+ : 'No timeout changes that; pass';
41
+ throw new Error(`${timedOut ? `gave up waiting after ${timeout}ms: ` : ''}${unsettled.join('; ')}. ${advice} allowUnsettled (--allowUnsettled) to capture the frame as it stands.`);
42
+ }
43
+ return { pending, tooLarge, unsettled };
44
+ }
package/esm/session.d.ts CHANGED
@@ -1,23 +1,35 @@
1
+ /**
2
+ * The decoded `session=` value that opens a session spec. Hand it to
3
+ * `URLSearchParams`, which encodes it once itself.
4
+ */
1
5
  export declare function sessionSpecParam(session: object): string;
6
+ /**
7
+ * The `session=` value pre-encoded, for a query string built by hand. Passed
8
+ * through `URLSearchParams` as well, it is encoded twice and the app fails to
9
+ * parse it.
10
+ */
2
11
  export declare function encodeSessionSpec(session: object): string;
3
12
  /**
4
- * The assembly a session spec's first view opens, if it names one.
5
- *
6
- * The session gate needs this because a spec can be loaded against any config —
7
- * `--hub hg38 --session spec.json` where the spec opens a different assembly is
8
- * legitimate, and defaulting the expectation to the hub name would fail a
9
- * perfectly good capture.
13
+ * The `session=` value that opens a session saved with "Export session...".
14
+ * Takes the exported `{ session: {...} }` document or the snapshot inside it.
15
+ */
16
+ export declare function savedSessionParam(session: object): string;
17
+ /** The snapshot inside an exported session document, or the snapshot itself. */
18
+ export declare function savedSnapshot(session: object): object;
19
+ /**
20
+ * The assembly the first view of a session spec or saved snapshot opens, if
21
+ * it names one.
10
22
  */
11
23
  export declare function assemblyFromSession(session: object): string | undefined;
12
24
  /**
13
- * Every trackId a session spec asks a view to open, including nested views (a
14
- * synteny or breakpoint-split spec puts its LGVs one level down).
15
- *
16
- * Used as the session gate's expectation: it is what turns "the browser loaded"
17
- * into "the tracks I named are open", and a spec is the one input where the
18
- * caller has not spelled those ids out separately.
25
+ * Every trackId a session spec or saved snapshot opens, nested views
26
+ * included.
19
27
  */
20
28
  export declare function trackIdsFromSession(session: object): string[];
29
+ /**
30
+ * `?config=…&session=…&sessionName=…`, for a caller that has an origin but no
31
+ * JBrowse instance URL to hand `jbrowseUrl`.
32
+ */
21
33
  export declare function sessionSpecQuery({ config, session, sessionName, }: {
22
34
  config: string;
23
35
  session: object;
package/esm/session.js CHANGED
@@ -1,31 +1,41 @@
1
- // The DECODED value of the `session=` query param that jbrowse-web reads to open
2
- // a declarative session without a saved-session backend: a `spec-` prefix plus
3
- // the session JSON. This is what the app sees after the query string is parsed,
4
- // so it is the right thing to hand to `URLSearchParams.set`, which does its own
5
- // encoding.
1
+ /**
2
+ * The decoded `session=` value that opens a session spec. Hand it to
3
+ * `URLSearchParams`, which encodes it once itself.
4
+ */
6
5
  export function sessionSpecParam(session) {
7
6
  return `spec-${JSON.stringify(session)}`;
8
7
  }
9
- // The same value pre-encoded, for callers that concatenate a query string by
10
- // hand rather than going through URL. Passing THIS to `URLSearchParams.set`
11
- // encodes it a second time, and the app then decodes once and parses `%7B…` as
12
- // JSON — so pick one path and stay on it. Shared so the runner and screenshot
13
- // generator can't drift on the encoding.
8
+ /**
9
+ * The `session=` value pre-encoded, for a query string built by hand. Passed
10
+ * through `URLSearchParams` as well, it is encoded twice and the app fails to
11
+ * parse it.
12
+ */
14
13
  export function encodeSessionSpec(session) {
15
14
  return encodeURIComponent(sessionSpecParam(session));
16
15
  }
17
16
  /**
18
- * The assembly a session spec's first view opens, if it names one.
19
- *
20
- * The session gate needs this because a spec can be loaded against any config —
21
- * `--hub hg38 --session spec.json` where the spec opens a different assembly is
22
- * legitimate, and defaulting the expectation to the hub name would fail a
23
- * perfectly good capture.
17
+ * The `session=` value that opens a session saved with "Export session...".
18
+ * Takes the exported `{ session: {...} }` document or the snapshot inside it.
19
+ */
20
+ export function savedSessionParam(session) {
21
+ return `json-${JSON.stringify({ session: savedSnapshot(session) })}`;
22
+ }
23
+ /** The snapshot inside an exported session document, or the snapshot itself. */
24
+ export function savedSnapshot(session) {
25
+ const inner = session.session;
26
+ return typeof inner === 'object' && inner !== null ? inner : session;
27
+ }
28
+ /**
29
+ * The assembly the first view of a session spec or saved snapshot opens, if
30
+ * it names one.
24
31
  */
25
32
  export function assemblyFromSession(session) {
26
33
  const find = (views) => {
27
34
  for (const view of views ?? []) {
28
- const found = view.assembly ?? find(view.views);
35
+ const found = [view.assembly ?? []].flat()[0] ??
36
+ view.displayedRegions?.[0]?.assemblyName ??
37
+ view.assemblyNames?.[0] ??
38
+ find(view.views);
29
39
  if (found) {
30
40
  return found;
31
41
  }
@@ -35,27 +45,32 @@ export function assemblyFromSession(session) {
35
45
  return find(session.views);
36
46
  }
37
47
  /**
38
- * Every trackId a session spec asks a view to open, including nested views (a
39
- * synteny or breakpoint-split spec puts its LGVs one level down).
40
- *
41
- * Used as the session gate's expectation: it is what turns "the browser loaded"
42
- * into "the tracks I named are open", and a spec is the one input where the
43
- * caller has not spelled those ids out separately.
48
+ * Every trackId a session spec or saved snapshot opens, nested views
49
+ * included.
44
50
  */
45
51
  export function trackIdsFromSession(session) {
52
+ const idOf = (t) => typeof t === 'string'
53
+ ? t
54
+ : (t.trackId ??
55
+ (typeof t.configuration === 'string'
56
+ ? t.configuration
57
+ : t.configuration?.trackId));
58
+ const idsOf = (tracks = []) => tracks.map(idOf).filter(id => id !== undefined);
46
59
  const collect = (views) => (views ?? []).flatMap(view => [
47
- // an entry may be a bare id or `{trackId, displaySnapshot}`; an object
48
- // without a trackId is malformed rather than a track to expect, so it
49
- // drops out here instead of gating on undefined
50
- ...(view.tracks ?? [])
51
- .map(t => (typeof t === 'string' ? t : t.trackId))
52
- .filter(id => id !== undefined),
60
+ ...idsOf(view.tracks?.flat()),
61
+ ...(view.levels ?? []).flatMap(l => idsOf(l.tracks)),
53
62
  ...collect(view.views),
54
63
  ]);
55
64
  return collect(session.views);
56
65
  }
57
- // Full `?config=…&session=…&sessionName=…` query string. `config` is inserted
58
- // as-is (callers that point at a remote absolute URL pre-encode it themselves).
66
+ /**
67
+ * `?config=…&session=…&sessionName=…`, for a caller that has an origin but no
68
+ * JBrowse instance URL to hand `jbrowseUrl`.
69
+ */
59
70
  export function sessionSpecQuery({ config, session, sessionName = 'Screenshot', }) {
60
- return `?config=${config}&session=${encodeSessionSpec(session)}&sessionName=${encodeURIComponent(sessionName)}`;
71
+ return `?${new URLSearchParams({
72
+ config,
73
+ session: sessionSpecParam(session),
74
+ sessionName,
75
+ })}`;
61
76
  }