@jbrowse/capture 5.0.0-beta.7 → 5.0.0-beta.9
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/README.md +46 -25
- package/esm/args.d.ts +2 -1
- package/esm/args.js +2 -1
- package/esm/bin.js +52 -29
- package/esm/browser.d.ts +0 -1
- package/esm/browser.js +5 -9
- package/esm/capture.d.ts +9 -8
- package/esm/capture.js +41 -57
- package/esm/catalog.d.ts +40 -0
- package/esm/catalog.js +103 -0
- package/esm/hub.d.ts +10 -0
- package/esm/hub.js +31 -16
- package/esm/imagePath.d.ts +1 -0
- package/esm/imagePath.js +8 -0
- package/esm/index.d.ts +8 -8
- package/esm/index.js +7 -7
- package/esm/instanceVersion.d.ts +8 -0
- package/esm/instanceVersion.js +24 -0
- package/esm/poll.d.ts +13 -0
- package/esm/poll.js +29 -0
- package/esm/ready.d.ts +20 -9
- package/esm/ready.js +37 -27
- package/esm/session.d.ts +13 -5
- package/esm/session.js +31 -12
- package/esm/sessionGate.d.ts +52 -43
- package/esm/sessionGate.js +82 -83
- package/esm/sessionOverflow.d.ts +11 -0
- package/esm/sessionOverflow.js +24 -0
- package/esm/url.d.ts +16 -6
- package/esm/url.js +34 -17
- package/esm/waits.d.ts +13 -16
- package/esm/waits.js +49 -81
- package/package.json +3 -3
package/esm/catalog.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { hubUrl } from './hub.js';
|
|
2
|
+
import { PUBLIC_INSTANCE } from './url.js';
|
|
3
|
+
const CATALOG_FETCH_TIMEOUT_MS = 30000;
|
|
4
|
+
const trackName = (track) => typeof track.name === 'string' ? track.name : '';
|
|
5
|
+
const assembliesOf = (catalog) => catalog.assemblies ?? (catalog.assembly ? [catalog.assembly] : []);
|
|
6
|
+
/**
|
|
7
|
+
* The canonical name of the assembly `input` names by name or alias, ignoring
|
|
8
|
+
* case. The canonical name is the one the app's census publishes, so the
|
|
9
|
+
* session gate compares against it rather than against an alias.
|
|
10
|
+
*/
|
|
11
|
+
export function resolveAssemblyName(catalog, input) {
|
|
12
|
+
const assemblies = assembliesOf(catalog);
|
|
13
|
+
const target = input.toLowerCase();
|
|
14
|
+
const found = assemblies.find(a => [a.name, ...(a.aliases ?? [])].some(n => n.toLowerCase() === target));
|
|
15
|
+
if (!found) {
|
|
16
|
+
const names = assemblies.map(a => a.name).join(', ');
|
|
17
|
+
throw new Error(`assembly "${input}" is not in the config, which has ${names || 'none'}`);
|
|
18
|
+
}
|
|
19
|
+
return found.name;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The trackId `input` names, resolved the way `jb2export --track` resolves one:
|
|
23
|
+
* an exact trackId, then the id with the assembly prefix a hosted config puts
|
|
24
|
+
* on every track (`clinvarMain` for `hg38-clinvarMain`), then a single
|
|
25
|
+
* case-insensitive match on the id, the unprefixed id or the display name. A
|
|
26
|
+
* miss names the tracks whose id or name contains what was typed.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveTrackId(tracks, input, assemblyName) {
|
|
29
|
+
const ids = new Set(tracks.map(t => t.trackId));
|
|
30
|
+
const prefixed = `${assemblyName}-${input}`;
|
|
31
|
+
if (ids.has(input)) {
|
|
32
|
+
return input;
|
|
33
|
+
}
|
|
34
|
+
if (ids.has(prefixed)) {
|
|
35
|
+
return prefixed;
|
|
36
|
+
}
|
|
37
|
+
const target = input.toLowerCase();
|
|
38
|
+
const prefix = `${assemblyName}-`.toLowerCase();
|
|
39
|
+
const loose = tracks.filter(t => {
|
|
40
|
+
const id = t.trackId.toLowerCase();
|
|
41
|
+
return (id === target ||
|
|
42
|
+
(id.startsWith(prefix) && id.slice(prefix.length) === target) ||
|
|
43
|
+
trackName(t).toLowerCase() === target);
|
|
44
|
+
});
|
|
45
|
+
if (loose.length === 1) {
|
|
46
|
+
return loose[0].trackId;
|
|
47
|
+
}
|
|
48
|
+
if (loose.length > 1) {
|
|
49
|
+
throw new Error(`--track "${input}" is ambiguous; matches: ${loose.map(t => t.trackId).join(', ')}`);
|
|
50
|
+
}
|
|
51
|
+
const suggestions = tracks
|
|
52
|
+
.filter(t => `${t.trackId} ${trackName(t)}`.toLowerCase().includes(target))
|
|
53
|
+
.slice(0, 8)
|
|
54
|
+
.map(t => t.trackId);
|
|
55
|
+
const hint = suggestions.length
|
|
56
|
+
? `. Did you mean: ${suggestions.join(', ')}?`
|
|
57
|
+
: '';
|
|
58
|
+
throw new Error(`--track "${input}" is not in the config${hint}`);
|
|
59
|
+
}
|
|
60
|
+
async function fetchCatalog(url) {
|
|
61
|
+
try {
|
|
62
|
+
const res = await fetch(url, {
|
|
63
|
+
signal: AbortSignal.timeout(CATALOG_FETCH_TIMEOUT_MS),
|
|
64
|
+
});
|
|
65
|
+
return res.ok ? (await res.json()) : undefined;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The same options with the assembly and tracks checked against the config
|
|
73
|
+
* and spelled the way the config spells them, before any browser launches: a
|
|
74
|
+
* typo fails here with suggestions rather than as a session-gate timeout.
|
|
75
|
+
*
|
|
76
|
+
* A config this cannot fetch or parse passes through unchanged, and the app
|
|
77
|
+
* reports it. A spec or saved session passes through too, since it may declare
|
|
78
|
+
* tracks of its own that no config lists.
|
|
79
|
+
*/
|
|
80
|
+
export async function resolveAgainstConfig(options) {
|
|
81
|
+
const { hub, config, assembly, tracks, spec, session, instance = PUBLIC_INSTANCE, } = options;
|
|
82
|
+
const wanted = assembly ?? hub;
|
|
83
|
+
const url = config
|
|
84
|
+
? new URL(config, instance).href
|
|
85
|
+
: hub
|
|
86
|
+
? hubUrl(hub)
|
|
87
|
+
: undefined;
|
|
88
|
+
if (spec || session || !url || (!wanted && !tracks?.length)) {
|
|
89
|
+
return options;
|
|
90
|
+
}
|
|
91
|
+
const catalog = await fetchCatalog(url);
|
|
92
|
+
if (!catalog) {
|
|
93
|
+
return options;
|
|
94
|
+
}
|
|
95
|
+
const assemblyName = wanted
|
|
96
|
+
? resolveAssemblyName(catalog, wanted)
|
|
97
|
+
: assembliesOf(catalog)[0]?.name;
|
|
98
|
+
return {
|
|
99
|
+
...options,
|
|
100
|
+
assembly: wanted ? assemblyName : undefined,
|
|
101
|
+
tracks: tracks?.map(t => resolveTrackId(catalog.tracks ?? [], t, assemblyName ?? '')),
|
|
102
|
+
};
|
|
103
|
+
}
|
package/esm/hub.d.ts
CHANGED
|
@@ -14,6 +14,16 @@ interface HubConfig {
|
|
|
14
14
|
* before naming them in a session.
|
|
15
15
|
*/
|
|
16
16
|
export declare function fetchHubConfig(hub: string): Promise<HubConfig>;
|
|
17
|
+
interface HostedAssembly {
|
|
18
|
+
name: string;
|
|
19
|
+
organism?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The UCSC assemblies genomes.jbrowse.org hosts, by name. GenArk accessions
|
|
24
|
+
* are hosted too, thousands of them, and are not in this list.
|
|
25
|
+
*/
|
|
26
|
+
export declare function listHubAssemblies(): Promise<HostedAssembly[]>;
|
|
17
27
|
/**
|
|
18
28
|
* The trackIds a hosted assembly publishes, optionally narrowed to those whose
|
|
19
29
|
* id or display name contains `filter`. Case-insensitive, because a hub carries
|
package/esm/hub.js
CHANGED
|
@@ -16,31 +16,46 @@ export function hubUrl(hub) {
|
|
|
16
16
|
? `${HUB_HOST}/hubs/genark/${genark[1]}/${genark[2]}/${genark[3]}/${genark[4]}/${hub}/config.json`
|
|
17
17
|
: `${HUB_HOST}/ucsc/${hub}/config.json`;
|
|
18
18
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
export async function fetchHubConfig(hub) {
|
|
25
|
-
const url = hubUrl(hub);
|
|
26
|
-
// Without the signal a stalled connection hangs `jb2capture list` forever:
|
|
27
|
-
// fetch has no timeout of its own, and this is the one request the tool makes
|
|
28
|
-
// outside puppeteer's budget.
|
|
19
|
+
const BROWSE_HINT = 'See https://genomes.jbrowse.org for the available assemblies.';
|
|
20
|
+
// Bounded: fetch has no timeout of its own, and these are the requests the tool
|
|
21
|
+
// makes outside puppeteer's budget, so without it a stalled connection hangs
|
|
22
|
+
// `jb2capture list` forever.
|
|
23
|
+
async function fetchHosted(url, what) {
|
|
29
24
|
const res = await fetch(url, {
|
|
30
25
|
signal: AbortSignal.timeout(HUB_FETCH_TIMEOUT_MS),
|
|
31
26
|
}).catch((error) => {
|
|
32
27
|
// The budget is only part of the diagnosis when it is what fired; naming it
|
|
33
28
|
// on a DNS failure reads as though the request had been given 30s to resolve.
|
|
34
29
|
const timedOut = error instanceof Error && error.name === 'TimeoutError';
|
|
35
|
-
throw new Error(
|
|
36
|
-
`(${error instanceof Error ? error.message : error}${timedOut ? `, after ${HUB_FETCH_TIMEOUT_MS}ms` : ''}). `
|
|
37
|
-
'See https://genomes.jbrowse.org for the available assemblies.');
|
|
30
|
+
throw new Error(`${what} could not be fetched from ${url} ` +
|
|
31
|
+
`(${error instanceof Error ? error.message : error}${timedOut ? `, after ${HUB_FETCH_TIMEOUT_MS}ms` : ''}). ${BROWSE_HINT}`);
|
|
38
32
|
});
|
|
39
33
|
if (!res.ok) {
|
|
40
|
-
throw new Error(
|
|
41
|
-
'See https://genomes.jbrowse.org for the available assemblies.');
|
|
34
|
+
throw new Error(`${what} not found (HTTP ${res.status} from ${url}). ${BROWSE_HINT}`);
|
|
42
35
|
}
|
|
43
|
-
return
|
|
36
|
+
return res.json();
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Fetch a hosted assembly config from genomes.jbrowse.org by UCSC database name
|
|
40
|
+
* or GenArk accession. Use it to discover which trackIds an assembly publishes
|
|
41
|
+
* before naming them in a session.
|
|
42
|
+
*/
|
|
43
|
+
export async function fetchHubConfig(hub) {
|
|
44
|
+
return (await fetchHosted(hubUrl(hub), `hub "${hub}"`));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The UCSC assemblies genomes.jbrowse.org hosts, by name. GenArk accessions
|
|
48
|
+
* are hosted too, thousands of them, and are not in this list.
|
|
49
|
+
*/
|
|
50
|
+
export async function listHubAssemblies() {
|
|
51
|
+
const { ucscGenomes = {} } = (await fetchHosted(`${HUB_HOST}/ucsc/list.json`, 'the hosted assembly list'));
|
|
52
|
+
return Object.entries(ucscGenomes)
|
|
53
|
+
.map(([name, { organism, description }]) => ({
|
|
54
|
+
name,
|
|
55
|
+
organism,
|
|
56
|
+
description,
|
|
57
|
+
}))
|
|
58
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
44
59
|
}
|
|
45
60
|
/**
|
|
46
61
|
* The trackIds a hosted assembly publishes, optionally narrowed to those whose
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function assertImagePath(out: string): void;
|
package/esm/imagePath.js
ADDED
|
@@ -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,12 +1,12 @@
|
|
|
1
1
|
export { captureJBrowse, openJBrowse } from './capture.ts';
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export { assemblyFromSession, encodeSessionSpec,
|
|
7
|
-
export {
|
|
8
|
-
export { BASE_CHROME_ARGS,
|
|
2
|
+
export { delay } from './poll.ts';
|
|
3
|
+
export { waitForFrame, waitForJBrowseReady } from './ready.ts';
|
|
4
|
+
export { describePendingDisplays, pendingDisplayStatesInPage, waitForSession, } from './sessionGate.ts';
|
|
5
|
+
export { jbrowseUrl } from './url.ts';
|
|
6
|
+
export { assemblyFromSession, encodeSessionSpec, sessionSpecQuery, trackIdsFromSession, } from './session.ts';
|
|
7
|
+
export { ANIMATING_DISPLAYS, PENDING_DISPLAYS, displayPainted, displaySettled, waitForAppReady, waitForAppSettled, waitForDisplayPhases, waitForDisplaysDone, waitForLoadingComplete, waitForQuiescent, waitForSelectorAttributed, waitForViewPhases, } from './waits.ts';
|
|
8
|
+
export { BASE_CHROME_ARGS, findChromeExecutable, isBrowserConsoleNoise, } from './browser.ts';
|
|
9
9
|
export type { CaptureOptions, CaptureResult, OpenOptions, OpenResult, } from './capture.ts';
|
|
10
10
|
export type { ReadyOptions, ReadyReport } from './ready.ts';
|
|
11
|
-
export type { PendingDisplay, SessionExpectations
|
|
11
|
+
export type { PendingDisplay, SessionExpectations } from './sessionGate.ts';
|
|
12
12
|
export type { JBrowseUrlOptions } from './url.ts';
|
package/esm/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { captureJBrowse, openJBrowse } from './capture.js';
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export { assemblyFromSession, encodeSessionSpec,
|
|
7
|
-
export {
|
|
8
|
-
export { BASE_CHROME_ARGS,
|
|
2
|
+
export { delay } from './poll.js';
|
|
3
|
+
export { waitForFrame, waitForJBrowseReady } from './ready.js';
|
|
4
|
+
export { describePendingDisplays, pendingDisplayStatesInPage, waitForSession, } from './sessionGate.js';
|
|
5
|
+
export { jbrowseUrl } from './url.js';
|
|
6
|
+
export { assemblyFromSession, encodeSessionSpec, sessionSpecQuery, trackIdsFromSession, } from './session.js';
|
|
7
|
+
export { ANIMATING_DISPLAYS, PENDING_DISPLAYS, displayPainted, displaySettled, waitForAppReady, waitForAppSettled, waitForDisplayPhases, waitForDisplaysDone, waitForLoadingComplete, waitForQuiescent, waitForSelectorAttributed, waitForViewPhases, } from './waits.js';
|
|
8
|
+
export { BASE_CHROME_ARGS, findChromeExecutable, isBrowserConsoleNoise, } from './browser.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refuse a JBrowse Web instance older than v5 before launching a browser at it.
|
|
3
|
+
*
|
|
4
|
+
* Every jbrowse-web build writes its version to `version.txt`. An instance that
|
|
5
|
+
* serves none, or serves its index page there, cannot be judged and passes; the
|
|
6
|
+
* session gate still fails on it, a timeout later.
|
|
7
|
+
*/
|
|
8
|
+
export declare function assertSupportedInstance(instance: string): Promise<void>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// v5 is the first release to publish `[data-app-phase]` and the census beside
|
|
2
|
+
// it, which are the only signals this package waits on.
|
|
3
|
+
const MIN_MAJOR = 5;
|
|
4
|
+
/**
|
|
5
|
+
* Refuse a JBrowse Web instance older than v5 before launching a browser at it.
|
|
6
|
+
*
|
|
7
|
+
* Every jbrowse-web build writes its version to `version.txt`. An instance that
|
|
8
|
+
* serves none, or serves its index page there, cannot be judged and passes; the
|
|
9
|
+
* session gate still fails on it, a timeout later.
|
|
10
|
+
*/
|
|
11
|
+
export async function assertSupportedInstance(instance) {
|
|
12
|
+
const text = await fetch(new URL('version.txt', instance), {
|
|
13
|
+
signal: AbortSignal.timeout(10000),
|
|
14
|
+
})
|
|
15
|
+
.then(res => (res.ok ? res.text() : ''))
|
|
16
|
+
.catch(() => '');
|
|
17
|
+
const version = text.trim();
|
|
18
|
+
const major = /^(\d+)\.\d+\.\d+/.exec(version)?.[1];
|
|
19
|
+
if (major !== undefined && Number(major) < MIN_MAJOR) {
|
|
20
|
+
throw new Error(`${instance} serves JBrowse ${version}, and @jbrowse/capture needs v${MIN_MAJOR} or later: ` +
|
|
21
|
+
'older builds publish no readiness marker to wait on. Point --instance at a ' +
|
|
22
|
+
`v${MIN_MAJOR} build.`);
|
|
23
|
+
}
|
|
24
|
+
}
|
package/esm/poll.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const delay: (ms: number) => Promise<void>;
|
|
2
|
+
/**
|
|
3
|
+
* Poll `read` until it has answered true for `holdMs` without a break, or
|
|
4
|
+
* `timeout` runs out. A `read` that throws ends the wait with its error.
|
|
5
|
+
*
|
|
6
|
+
* Polled from Node rather than in the page: chrome throttles in-page timers and
|
|
7
|
+
* rAF once a tab is not visible, which is the state a headless capture sits in.
|
|
8
|
+
*/
|
|
9
|
+
export declare function holdTrue(read: () => Promise<boolean>, { holdMs, timeout, pollMs, }: {
|
|
10
|
+
holdMs: number;
|
|
11
|
+
timeout: number;
|
|
12
|
+
pollMs: number;
|
|
13
|
+
}): Promise<boolean>;
|
package/esm/poll.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const delay = (ms) => new Promise(resolve => {
|
|
2
|
+
setTimeout(resolve, ms);
|
|
3
|
+
});
|
|
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
|
+
*
|
|
8
|
+
* Polled from Node rather than in the page: chrome throttles in-page timers and
|
|
9
|
+
* rAF once a tab is not visible, which is the state a headless capture sits in.
|
|
10
|
+
*/
|
|
11
|
+
export async function holdTrue(read, { holdMs, timeout, pollMs, }) {
|
|
12
|
+
const deadline = Date.now() + timeout;
|
|
13
|
+
let since;
|
|
14
|
+
while (Date.now() < deadline) {
|
|
15
|
+
const ok = await read();
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
if (ok) {
|
|
18
|
+
since ??= now;
|
|
19
|
+
if (now - since >= holdMs) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
since = undefined;
|
|
25
|
+
}
|
|
26
|
+
await delay(pollMs);
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
package/esm/ready.d.ts
CHANGED
|
@@ -17,14 +17,17 @@ export interface ReadyOptions extends SessionExpectations {
|
|
|
17
17
|
allowUnsettled?: boolean;
|
|
18
18
|
}
|
|
19
19
|
export interface ReadyReport {
|
|
20
|
-
/** Displays still reporting unpainted at the end of the wait. */
|
|
21
|
-
pending: string[];
|
|
22
20
|
/**
|
|
23
|
-
*
|
|
24
|
-
* separates a slow fetch from a
|
|
25
|
-
*
|
|
21
|
+
* Displays still reporting unpainted, or canceled, at the end of the wait,
|
|
22
|
+
* each with the phase it publishes: what separates a slow fetch from a
|
|
23
|
+
* display that says it finished without painting.
|
|
26
24
|
*/
|
|
27
|
-
|
|
25
|
+
pending: PendingDisplay[];
|
|
26
|
+
/**
|
|
27
|
+
* Displays showing "too much data" instead of their features. Not a failure,
|
|
28
|
+
* so never in `unsettled`, but worth saying: the image shows a banner there.
|
|
29
|
+
*/
|
|
30
|
+
tooLarge: PendingDisplay[];
|
|
28
31
|
/** Wait stages that hit their timeout instead of being satisfied. */
|
|
29
32
|
unsettled: string[];
|
|
30
33
|
}
|
|
@@ -43,7 +46,15 @@ export interface ReadyReport {
|
|
|
43
46
|
*
|
|
44
47
|
* Then one negative gate that is only meaningful after them, and answers the
|
|
45
48
|
* question the marker does not: the marker is about WORK, and a display whose
|
|
46
|
-
* fetch failed is not working — it reads `ready` over an error banner
|
|
47
|
-
* `data-display-drawn` is the stricter gate
|
|
49
|
+
* fetch failed is not working — it reads `ready` over an error banner, and over
|
|
50
|
+
* a user's cancel. `data-display-drawn` is the stricter gate, and the census
|
|
51
|
+
* after it names a canceled display whatever it drew.
|
|
52
|
+
*/
|
|
53
|
+
export declare function waitForJBrowseReady(page: Page, options?: ReadyOptions): Promise<ReadyReport>;
|
|
54
|
+
/**
|
|
55
|
+
* The half of `waitForJBrowseReady` after the session gate: the marker held,
|
|
56
|
+
* every display painted, and the census of what did not. Run again after
|
|
57
|
+
* anything that changes the frame without changing the session, such as a
|
|
58
|
+
* viewport resize.
|
|
48
59
|
*/
|
|
49
|
-
export declare function
|
|
60
|
+
export declare function waitForFrame(page: Page, { timeout, settleMs, allowUnsettled, }?: Omit<ReadyOptions, keyof SessionExpectations>): Promise<ReadyReport>;
|
package/esm/ready.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { delay } from './poll.js';
|
|
2
|
+
import { describePendingDisplays, pendingDisplayStates, tooLargeDisplaysInPage, waitForSession, } from './sessionGate.js';
|
|
3
|
+
import { waitForAppSettled, waitForDisplaysDone } from './waits.js';
|
|
3
4
|
/**
|
|
4
5
|
* Wait until a JBrowse page has finished loading AND finished drawing.
|
|
5
6
|
*
|
|
@@ -15,26 +16,25 @@ import { delay, hasAppReadyMarker, waitForAppSettled, waitForDisplaysDone, } fro
|
|
|
15
16
|
*
|
|
16
17
|
* Then one negative gate that is only meaningful after them, and answers the
|
|
17
18
|
* question the marker does not: the marker is about WORK, and a display whose
|
|
18
|
-
* fetch failed is not working — it reads `ready` over an error banner
|
|
19
|
-
* `data-display-drawn` is the stricter gate
|
|
19
|
+
* fetch failed is not working — it reads `ready` over an error banner, and over
|
|
20
|
+
* a user's cancel. `data-display-drawn` is the stricter gate, and the census
|
|
21
|
+
* after it names a canceled display whatever it drew.
|
|
20
22
|
*/
|
|
21
|
-
export async function waitForJBrowseReady(page,
|
|
23
|
+
export async function waitForJBrowseReady(page, options = {}) {
|
|
24
|
+
// The census the gate reads shares its element with `[data-app-phase]`, so
|
|
25
|
+
// once it passes the marker is there too. Handed the options whole, so every
|
|
26
|
+
// session expectation reaches it, `views` included.
|
|
27
|
+
await waitForSession(page, options);
|
|
28
|
+
return waitForFrame(page, options);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The half of `waitForJBrowseReady` after the session gate: the marker held,
|
|
32
|
+
* every display painted, and the census of what did not. Run again after
|
|
33
|
+
* anything that changes the frame without changing the session, such as a
|
|
34
|
+
* viewport resize.
|
|
35
|
+
*/
|
|
36
|
+
export async function waitForFrame(page, { timeout = 60000, settleMs = 0, allowUnsettled = false, } = {}) {
|
|
22
37
|
const unsettled = [];
|
|
23
|
-
// 0. the session exists and holds what was asked for. Positive, and throws.
|
|
24
|
-
await waitForSession(page, { assembly, trackIds, timeout });
|
|
25
|
-
// 1. and the build publishes the marker at all. Said out loud rather than
|
|
26
|
-
// worked around: a chain built from the remaining signals is satisfied by
|
|
27
|
-
// a page that has not started, so the fallback that used to run here
|
|
28
|
-
// reported success on an empty browser unless it was also given a
|
|
29
|
-
// seen-busy-then-quiet gate to compensate. One positive selector replaces
|
|
30
|
-
// all of it, and a build too old to have it needs a newer --instance, not
|
|
31
|
-
// a wait chain that cannot fail.
|
|
32
|
-
if (!(await hasAppReadyMarker(page))) {
|
|
33
|
-
throw new Error('this page publishes no [data-app-phase], so there is no positive signal ' +
|
|
34
|
-
'that it has finished — every other readiness attribute is an absence ' +
|
|
35
|
-
'an app that has not started also satisfies. Point --instance at a ' +
|
|
36
|
-
'JBrowse build that publishes it.');
|
|
37
|
-
}
|
|
38
38
|
const ready = await waitForAppSettled(page, { timeout });
|
|
39
39
|
if (!ready) {
|
|
40
40
|
unsettled.push('the app never held itself ready');
|
|
@@ -56,18 +56,28 @@ export async function waitForJBrowseReady(page, { timeout = 60000, settleMs = 0,
|
|
|
56
56
|
// that painted during `settleMs` was absent from `pending` and still failed
|
|
57
57
|
// the run, while the CLI's own warning recommended raising `--settle` to fix
|
|
58
58
|
// exactly that.
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
const pending = await pendingDisplayStates(page);
|
|
60
|
+
const canceled = pending.filter(d => d.phase === 'canceled');
|
|
61
|
+
const unpainted = pending.filter(d => d.phase !== 'canceled');
|
|
62
|
+
if (unpainted.length > 0) {
|
|
63
|
+
unsettled.push(`display(s) never painted: ${describePendingDisplays(unpainted)}`);
|
|
64
|
+
}
|
|
65
|
+
if (canceled.length > 0) {
|
|
66
|
+
unsettled.push(`display(s) canceled, which lasts until Retry: ${describePendingDisplays(canceled)}`);
|
|
62
67
|
}
|
|
63
68
|
if (!allowUnsettled && unsettled.length > 0) {
|
|
64
69
|
// Throwing is the point. Each stage swallows its own timeout so a slow page
|
|
65
70
|
// is not failed for being slow, which historically meant the run ended with
|
|
66
71
|
// an image and an exit code of 0 whether it had settled or not. A caller
|
|
67
72
|
// that genuinely wants the frame anyway asks for it by name.
|
|
68
|
-
throw new Error(
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
throw new Error(unsettled.length === 1 && canceled.length > 0
|
|
74
|
+
? `${unsettled[0]}. No timeout lifts a cancel; press Retry on the ` +
|
|
75
|
+
'track, or pass allowUnsettled (--allowUnsettled) to capture the ' +
|
|
76
|
+
'frame as it stands.'
|
|
77
|
+
: `gave up waiting after ${timeout}ms: ${unsettled.join('; ')}. ` +
|
|
78
|
+
'Raise the timeout, or pass allowUnsettled (--allowUnsettled) to ' +
|
|
79
|
+
'capture the frame as it stands.');
|
|
71
80
|
}
|
|
72
|
-
|
|
81
|
+
const tooLarge = await page.evaluate(tooLargeDisplaysInPage);
|
|
82
|
+
return { pending, tooLarge, unsettled };
|
|
73
83
|
}
|
package/esm/session.d.ts
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
export declare function sessionSpecParam(session: object): string;
|
|
2
2
|
export declare function encodeSessionSpec(session: object): string;
|
|
3
3
|
/**
|
|
4
|
-
* The
|
|
4
|
+
* The `session=` value that opens a session saved with "Export session...".
|
|
5
|
+
* Takes the exported `{ session: {...} }` document or the snapshot inside it.
|
|
6
|
+
*/
|
|
7
|
+
export declare function savedSessionParam(session: object): string;
|
|
8
|
+
/** The snapshot inside an exported session document, or the snapshot itself. */
|
|
9
|
+
export declare function savedSnapshot(session: object): object;
|
|
10
|
+
/**
|
|
11
|
+
* The assembly the first view of a session spec or saved snapshot opens, if it
|
|
12
|
+
* names one.
|
|
5
13
|
*
|
|
6
14
|
* The session gate needs this because a spec can be loaded against any config —
|
|
7
|
-
* `--hub hg38 --
|
|
15
|
+
* `--hub hg38 --spec spec.json` where the spec opens a different assembly is
|
|
8
16
|
* legitimate, and defaulting the expectation to the hub name would fail a
|
|
9
17
|
* perfectly good capture.
|
|
10
18
|
*/
|
|
11
19
|
export declare function assemblyFromSession(session: object): string | undefined;
|
|
12
20
|
/**
|
|
13
|
-
* Every trackId a session spec
|
|
14
|
-
* synteny or breakpoint-split
|
|
21
|
+
* Every trackId a session spec or saved snapshot opens, including nested views
|
|
22
|
+
* (a synteny or breakpoint-split view puts its LGVs one level down).
|
|
15
23
|
*
|
|
16
24
|
* Used as the session gate's expectation: it is what turns "the browser loaded"
|
|
17
|
-
* into "the tracks I named are open", and a
|
|
25
|
+
* into "the tracks I named are open", and a session is the one input where the
|
|
18
26
|
* caller has not spelled those ids out separately.
|
|
19
27
|
*/
|
|
20
28
|
export declare function trackIdsFromSession(session: object): string[];
|
package/esm/session.js
CHANGED
|
@@ -15,17 +15,33 @@ export function encodeSessionSpec(session) {
|
|
|
15
15
|
return encodeURIComponent(sessionSpecParam(session));
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
* The
|
|
18
|
+
* The `session=` value that opens a session saved with "Export session...".
|
|
19
|
+
* Takes the exported `{ session: {...} }` document or the snapshot inside it.
|
|
20
|
+
*/
|
|
21
|
+
export function savedSessionParam(session) {
|
|
22
|
+
return `json-${JSON.stringify({ session: savedSnapshot(session) })}`;
|
|
23
|
+
}
|
|
24
|
+
/** The snapshot inside an exported session document, or the snapshot itself. */
|
|
25
|
+
export function savedSnapshot(session) {
|
|
26
|
+
const inner = session.session;
|
|
27
|
+
return typeof inner === 'object' && inner !== null ? inner : session;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The assembly the first view of a session spec or saved snapshot opens, if it
|
|
31
|
+
* names one.
|
|
19
32
|
*
|
|
20
33
|
* The session gate needs this because a spec can be loaded against any config —
|
|
21
|
-
* `--hub hg38 --
|
|
34
|
+
* `--hub hg38 --spec spec.json` where the spec opens a different assembly is
|
|
22
35
|
* legitimate, and defaulting the expectation to the hub name would fail a
|
|
23
36
|
* perfectly good capture.
|
|
24
37
|
*/
|
|
25
38
|
export function assemblyFromSession(session) {
|
|
26
39
|
const find = (views) => {
|
|
27
40
|
for (const view of views ?? []) {
|
|
28
|
-
const found = view.assembly ??
|
|
41
|
+
const found = [view.assembly ?? []].flat()[0] ??
|
|
42
|
+
view.displayedRegions?.[0]?.assemblyName ??
|
|
43
|
+
view.assemblyNames?.[0] ??
|
|
44
|
+
find(view.views);
|
|
29
45
|
if (found) {
|
|
30
46
|
return found;
|
|
31
47
|
}
|
|
@@ -35,20 +51,23 @@ export function assemblyFromSession(session) {
|
|
|
35
51
|
return find(session.views);
|
|
36
52
|
}
|
|
37
53
|
/**
|
|
38
|
-
* Every trackId a session spec
|
|
39
|
-
* synteny or breakpoint-split
|
|
54
|
+
* Every trackId a session spec or saved snapshot opens, including nested views
|
|
55
|
+
* (a synteny or breakpoint-split view puts its LGVs one level down).
|
|
40
56
|
*
|
|
41
57
|
* Used as the session gate's expectation: it is what turns "the browser loaded"
|
|
42
|
-
* into "the tracks I named are open", and a
|
|
58
|
+
* into "the tracks I named are open", and a session is the one input where the
|
|
43
59
|
* caller has not spelled those ids out separately.
|
|
44
60
|
*/
|
|
45
61
|
export function trackIdsFromSession(session) {
|
|
46
|
-
// an entry
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
62
|
+
// an entry that names no trackId is malformed rather than a track to expect,
|
|
63
|
+
// so it drops out here instead of gating on undefined
|
|
64
|
+
const idOf = (t) => typeof t === 'string'
|
|
65
|
+
? t
|
|
66
|
+
: (t.trackId ??
|
|
67
|
+
(typeof t.configuration === 'string'
|
|
68
|
+
? t.configuration
|
|
69
|
+
: t.configuration?.trackId));
|
|
70
|
+
const idsOf = (tracks) => (tracks ?? []).map(idOf).filter(id => id !== undefined);
|
|
52
71
|
const collect = (views) => (views ?? []).flatMap(view => [
|
|
53
72
|
// a nested entry is a synteny spec's per-level list; these used to be
|
|
54
73
|
// filtered out as malformed, so a synteny --session gated on nothing
|