@jbrowse/capture 4.3.0

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.
@@ -0,0 +1,113 @@
1
+ import type { Instrumentation, PendingDisplay, SessionExpectations } from './sessionGate.ts';
2
+ import type { JBrowseUrlOptions } from './url.ts';
3
+ import type { Browser, Page } from 'puppeteer';
4
+ export interface ReadyOptions extends SessionExpectations {
5
+ /** Budget for each individual wait stage. */
6
+ timeout?: number;
7
+ /**
8
+ * Also wait out adapter "Downloading…" status text, which can outlive the
9
+ * loading overlay when a track streams a large remote file.
10
+ */
11
+ waitForDownloads?: boolean;
12
+ /** Extra settle after everything reports done, for animations and tooltips. */
13
+ settleMs?: number;
14
+ /**
15
+ * Skip the session gate. Only for a page that is not jbrowse-web and so
16
+ * publishes no `window.JBrowseSession` — an embedded component, say. You lose
17
+ * the only check that the data you asked for is the data on screen.
18
+ */
19
+ expectSession?: boolean;
20
+ /**
21
+ * Return the report instead of throwing when a wait stage times out. For
22
+ * capturing a page that is deliberately mid-load, or accepting a known-slow
23
+ * one. The unsettled stages are still listed in the report either way.
24
+ */
25
+ allowUnsettled?: boolean;
26
+ }
27
+ export interface ReadyReport {
28
+ /** Displays still reporting unpainted at the end of the wait. */
29
+ pending: string[];
30
+ /**
31
+ * The same displays with the phase each one publishes, which is what
32
+ * separates a slow fetch from a display that says it finished without
33
+ * painting. Empty whenever `pending` is.
34
+ */
35
+ pendingStates: PendingDisplay[];
36
+ /**
37
+ * Whether every display's paint state was actually measurable — false when
38
+ * tracks are open but the build publishes no `data-display-drawn`. With this
39
+ * false, an empty `pending` means "cannot tell", not "all done".
40
+ */
41
+ paintContract: boolean;
42
+ /** Wait stages that hit their timeout instead of being satisfied. */
43
+ unsettled: string[];
44
+ /**
45
+ * Which readiness attributes the build published, read once the session was
46
+ * up. Every `false` here is a wait that could not fail rather than one that
47
+ * passed, and the chain compensates for it — see LEGACY_QUIET_MS.
48
+ */
49
+ instrumentation: Instrumentation;
50
+ /**
51
+ * Whether the page published `[data-app-phase]`, the one positive readiness
52
+ * selector. True means the wait was that selector and nothing else; false
53
+ * means the build predates it and the fallback chain ran.
54
+ */
55
+ appMarker: boolean;
56
+ }
57
+ /**
58
+ * Wait until a JBrowse page has finished loading AND finished drawing.
59
+ *
60
+ * One positive gate, then the negative ones. The order is the whole point: the
61
+ * DOM waits all pass on a page that has not started yet, so without the session
62
+ * gate in front the chain returns in under a second (see sessionGate.ts). After
63
+ * it, each stage is only meaningful once the previous has passed — a view still
64
+ * resolving its assembly has mounted no displays, and a display that has mounted
65
+ * but not fetched has nothing to be drawn yet.
66
+ */
67
+ export declare function waitForJBrowseReady(page: Page, { timeout, waitForDownloads, settleMs, expectSession, assembly, trackIds, allowUnsettled, }?: ReadyOptions): Promise<ReadyReport>;
68
+ export interface OpenOptions extends JBrowseUrlOptions, ReadyOptions {
69
+ width?: number;
70
+ height?: number;
71
+ /** 2 renders a retina-density image, which is what a figure usually wants. */
72
+ deviceScaleFactor?: number;
73
+ headless?: boolean;
74
+ /** Chrome binary. Defaults to $CHROME_PATH, a system Chrome, then Puppeteer's own. */
75
+ executablePath?: string;
76
+ /** Extra Chrome flags, appended to the defaults. */
77
+ args?: string[];
78
+ /** Called with each page console message that is not known GPU noise. */
79
+ onConsole?: (text: string) => void;
80
+ }
81
+ export interface OpenResult extends ReadyReport {
82
+ browser: Browser;
83
+ page: Page;
84
+ url: string;
85
+ }
86
+ /**
87
+ * Launch a browser, navigate to a JBrowse session, and wait until it has
88
+ * rendered. The caller owns the returned browser and must close it.
89
+ *
90
+ * Use this when the screenshot is not the end of the job: clicking a feature,
91
+ * reading state back out of `window.JBrowseSession`, capturing a dialog. For a
92
+ * plain image, `captureJBrowse` wraps the whole thing.
93
+ *
94
+ * The assembly and track count asked for in the URL become the session gate's
95
+ * expectations unless you override them, so a mistyped trackId fails here rather
96
+ * than producing an image of an empty browser.
97
+ */
98
+ export declare function openJBrowse(options?: OpenOptions): Promise<OpenResult>;
99
+ export interface CaptureOptions extends OpenOptions {
100
+ /** PNG path to write. Omit to get the buffer back and write it yourself. */
101
+ out?: string;
102
+ /** Capture the whole scrollable page rather than the viewport. */
103
+ fullPage?: boolean;
104
+ }
105
+ export interface CaptureResult extends ReadyReport {
106
+ url: string;
107
+ image: Uint8Array;
108
+ }
109
+ /**
110
+ * Open a JBrowse session, wait for it to render, screenshot it, close the
111
+ * browser. The one-call form.
112
+ */
113
+ export declare function captureJBrowse(options?: CaptureOptions): Promise<CaptureResult>;
package/esm/capture.js ADDED
@@ -0,0 +1,273 @@
1
+ import puppeteer from 'puppeteer';
2
+ import { BASE_CHROME_ARGS, findChromeExecutable, isBrowserConsoleNoise, } from './browser.js';
3
+ import { assemblyFromSession, trackIdsFromSession } from './session.js';
4
+ import { describePendingDisplays, hasPaintContract, pendingDisplayStates, readInstrumentation, readSessionSummary, waitForSession, } from './sessionGate.js';
5
+ import { jbrowseUrl } from './url.js';
6
+ import { delay, hasAppReadyMarker, waitForAppReady, waitForDisplayPhases, waitForDisplaysDone, waitForLoadingComplete, waitForQuiescent, waitForQuietPeriod, waitForViewPhases, } from './waits.js';
7
+ // A build with no per-display paint attributes gives no signal between "the
8
+ // overlay cleared" and "the canvas has pixels on it", so the only thing left is
9
+ // to wait. Sized from the measured gap on jbrowse.org/code/jb2/latest between
10
+ // the overlay clearing and the tracks being fully drawn.
11
+ const LEGACY_PAINT_SETTLE_MS = 1500;
12
+ // ...and the gap at the OTHER end, which is the one that lands a capture on an
13
+ // app that has drawn nothing at all. Measured on the same instance with two
14
+ // remote tracks: the session gate is satisfied at ~2.5s, and the loading overlay
15
+ // — the only remaining signal there — does not go up until ~3.5s. An
16
+ // instrumented build has no such window: `data-display-phase` appears in the
17
+ // same frame as the display it belongs to.
18
+ //
19
+ // So on an uninstrumented build the app has to be seen to START working, and
20
+ // then hold still, before it counts as finished. The window is how long the
21
+ // first indicator gets to appear before "nothing to wait for" is the better
22
+ // reading; the quiet is how long the idle then has to hold.
23
+ const LEGACY_BUSY_WINDOW_MS = 4000;
24
+ const LEGACY_QUIET_MS = 2000;
25
+ /**
26
+ * Wait until a JBrowse page has finished loading AND finished drawing.
27
+ *
28
+ * One positive gate, then the negative ones. The order is the whole point: the
29
+ * DOM waits all pass on a page that has not started yet, so without the session
30
+ * gate in front the chain returns in under a second (see sessionGate.ts). After
31
+ * it, each stage is only meaningful once the previous has passed — a view still
32
+ * resolving its assembly has mounted no displays, and a display that has mounted
33
+ * but not fetched has nothing to be drawn yet.
34
+ */
35
+ export async function waitForJBrowseReady(page, { timeout = 60000, waitForDownloads = true, settleMs = 0, expectSession = true, assembly, trackIds, allowUnsettled = false, } = {}) {
36
+ const unsettled = [];
37
+ const stage = async (name, work) => {
38
+ if (!(await work)) {
39
+ unsettled.push(name);
40
+ }
41
+ };
42
+ // The two hard waits reject with puppeteer's own `Waiting failed: Nms
43
+ // exceeded`, which names neither the stage nor the selector — the whole
44
+ // failure mode this module exists to avoid, arriving as an error message
45
+ // instead of as a blank image. Say which gate it was and what to do.
46
+ const required = async (name, work) => {
47
+ try {
48
+ return await work;
49
+ }
50
+ catch {
51
+ throw new Error(`gave up waiting after ${timeout}ms: ${name}. Raise the timeout if the ` +
52
+ 'page is merely slow; if it never finishes, open the same URL in a ' +
53
+ 'browser — this gate has no content to fall through to.');
54
+ }
55
+ };
56
+ // 0. the session exists and holds what was asked for. Positive, and throws.
57
+ if (expectSession) {
58
+ await waitForSession(page, { assembly, trackIds, timeout });
59
+ }
60
+ // 1. THE WHOLE ANSWER, on any build that has it: the session renders
61
+ // `[data-app-phase="ready"]` when no view is resolving an assembly and no
62
+ // display is fetching. It is positive, so unlike everything below it
63
+ // cannot be satisfied by an app that has not started, and there is nothing
64
+ // to assemble — wait for the selector and stop.
65
+ //
66
+ // Everything after this point is the fallback for a deployment older than
67
+ // the marker, and can be deleted the day the oldest supported build has
68
+ // it.
69
+ if (await hasAppReadyMarker(page)) {
70
+ const ready = await waitForAppReady(page, { timeout });
71
+ if (!ready) {
72
+ unsettled.push('the app never reported itself ready');
73
+ }
74
+ // One thing the marker does not answer, so this stage stays even here: the
75
+ // marker is about WORK, and a display whose fetch failed is not working. It
76
+ // reads `ready` over an error banner, which is a correct answer to a
77
+ // different question than a capture is asking. `data-display-drawn` is the
78
+ // stricter gate — the two comparative canvases publish it from `settled`,
79
+ // which holds an error open deliberately so a golden regenerated during an
80
+ // outage fails here instead of absorbing the banner as expected output.
81
+ // Ordered after the marker rather than instead of it, which is what makes an
82
+ // absence meaningful (see waitForDisplaysDone), and free on a page that has
83
+ // no such canvas.
84
+ await stage('a display never reported its first paint', waitForDisplaysDone(page, timeout));
85
+ if (settleMs > 0) {
86
+ await delay(settleMs);
87
+ }
88
+ const pendingStates = await pendingDisplayStates(page);
89
+ const report = {
90
+ pending: pendingStates.map(d => d.name),
91
+ pendingStates,
92
+ paintContract: await hasPaintContract(page),
93
+ unsettled,
94
+ instrumentation: await readInstrumentation(page),
95
+ appMarker: true,
96
+ };
97
+ if (!allowUnsettled && unsettled.length > 0) {
98
+ throw new Error(unsettledMessage(timeout, unsettled, pendingStates));
99
+ }
100
+ return report;
101
+ }
102
+ // 1b. what an older build can be asked instead. On one that publishes none of
103
+ // these, the stages below are not gates at all: they are assertions about
104
+ // absent attributes that no page can fail.
105
+ const instrumentation = await readInstrumentation(page);
106
+ const instrumented = instrumentation.displayPhase || instrumentation.displayDrawn;
107
+ // A page with no tracks open has nothing to load, so the quiet gate below
108
+ // would be a fixed sleep for an import form or a menu shot.
109
+ const summary = await readSessionSummary(page);
110
+ const needsQuietGate = !instrumented && (summary?.trackIds.length ?? 0) > 0;
111
+ // 2. the view has an assembly and its React component has arrived. Also not
112
+ // best-effort: a view stuck here has no content to fall through to.
113
+ await required('a view never left its loading phase (still resolving its assembly, or its ' +
114
+ 'lazily-imported component never arrived)', waitForViewPhases(page, timeout));
115
+ // 2. no track is still fetching. One call, two outcomes: the overlay half is
116
+ // required (an overlay that never clears is a fetch that never finished,
117
+ // with nothing behind it) and throws; the "Downloading…" half is
118
+ // best-effort and comes back as the boolean.
119
+ const downloadsSettled = await required('the loading overlay never cleared (a track fetch never finished)', waitForLoadingComplete(page, { timeout, waitForDownloads }));
120
+ if (!downloadsSettled) {
121
+ unsettled.push('a track was still downloading');
122
+ }
123
+ await stage('a display was still in its loading phase', waitForDisplayPhases(page, timeout));
124
+ // 3. no display is still pending its first paint
125
+ await stage('a display never reported its first paint', waitForDisplaysDone(page, timeout));
126
+ // 4. no visible "Loading…/Rendering…/Computing…" text remains, which is how
127
+ // the views that publish no phase attribute report themselves
128
+ await stage('a "Loading…/Rendering…" label was still on screen', waitForQuiescent(page, { timeout }));
129
+ // 5. and on a build that answered none of the above, the one gate that does
130
+ // not depend on an attribute existing: the app has to hold still. Every
131
+ // stage before this one passed the moment it was asked, so without it the
132
+ // chain returns while the first fetch is still being set up.
133
+ if (needsQuietGate) {
134
+ await stage('the app never went quiet for ' +
135
+ `${LEGACY_QUIET_MS}ms (this build publishes no readiness attributes, ` +
136
+ 'so being seen to work and then stop is the only finished signal there ' +
137
+ 'is)', waitForQuietPeriod(page, {
138
+ quietMs: LEGACY_QUIET_MS,
139
+ busyWindowMs: LEGACY_BUSY_WINDOW_MS,
140
+ timeout,
141
+ }));
142
+ }
143
+ const paintContract = await hasPaintContract(page);
144
+ if (!paintContract) {
145
+ await delay(LEGACY_PAINT_SETTLE_MS);
146
+ }
147
+ if (settleMs > 0) {
148
+ await delay(settleMs);
149
+ }
150
+ const pendingStates = await pendingDisplayStates(page);
151
+ const report = {
152
+ pending: pendingStates.map(d => d.name),
153
+ pendingStates,
154
+ paintContract,
155
+ unsettled,
156
+ instrumentation,
157
+ appMarker: false,
158
+ };
159
+ if (!allowUnsettled && unsettled.length > 0) {
160
+ // Throwing is the point. Each of these stages swallows its own timeout so a
161
+ // slow page is not failed for being slow, which historically meant the run
162
+ // ended with an image and an exit code of 0 whether it had settled or not.
163
+ // A caller that genuinely wants the frame anyway asks for it by name.
164
+ throw new Error(unsettledMessage(timeout, unsettled, pendingStates));
165
+ }
166
+ return report;
167
+ }
168
+ /**
169
+ * What a timed-out wait says. The stage names alone were the whole message, and
170
+ * they name the QUESTION rather than the answer — "a display never reported its
171
+ * first paint" reads identically for a slow fetch, a failed one and a display
172
+ * that never had a canvas to paint. Appending the census answers it, and the
173
+ * `ready` case is the one that most changes what a reader does next: a longer
174
+ * timeout is the fix for `loading` and never the fix for that.
175
+ */
176
+ function unsettledMessage(timeout, unsettled, pending) {
177
+ return (`gave up waiting after ${timeout}ms: ${unsettled.join('; ')}. ${pending.length > 0
178
+ ? `Still unpainted: ${describePendingDisplays(pending)}. `
179
+ : ''}Raise the timeout, or pass allowUnsettled (--allowUnsettled) to ` +
180
+ `capture the frame as it stands.`);
181
+ }
182
+ /**
183
+ * Launch a browser, navigate to a JBrowse session, and wait until it has
184
+ * rendered. The caller owns the returned browser and must close it.
185
+ *
186
+ * Use this when the screenshot is not the end of the job: clicking a feature,
187
+ * reading state back out of `window.JBrowseSession`, capturing a dialog. For a
188
+ * plain image, `captureJBrowse` wraps the whole thing.
189
+ *
190
+ * The assembly and track count asked for in the URL become the session gate's
191
+ * expectations unless you override them, so a mistyped trackId fails here rather
192
+ * than producing an image of an empty browser.
193
+ */
194
+ export async function openJBrowse(options = {}) {
195
+ const { width = 1400, height = 900, deviceScaleFactor = 2, headless = true, executablePath = findChromeExecutable(), args = [], onConsole, timeout, trackIds,
196
+ // Pulled out only to keep them out of `urlOptions`, which becomes the
197
+ // query string. They reach the ready wait through the `...options` spread
198
+ // below rather than from here.
199
+ waitForDownloads: _waitForDownloads, settleMs: _settleMs, expectSession: _expectSession, allowUnsettled: _allowUnsettled,
200
+ // `assembly` is deliberately NOT pulled out here: it is both a URL option
201
+ // (which assembly to open) and the session gate's expectation (which
202
+ // assembly must end up open), and destructuring it for the second use would
203
+ // have dropped it from the first, silently.
204
+ ...urlOptions } = options;
205
+ const url = jbrowseUrl(urlOptions);
206
+ const browser = await puppeteer.launch({
207
+ headless,
208
+ executablePath,
209
+ args: [...BASE_CHROME_ARGS, ...args],
210
+ });
211
+ try {
212
+ const page = await browser.newPage();
213
+ await page.setViewport({ width, height, deviceScaleFactor });
214
+ if (onConsole) {
215
+ page.on('console', msg => {
216
+ const text = msg.text();
217
+ if (!isBrowserConsoleNoise(text)) {
218
+ onConsole(text);
219
+ }
220
+ });
221
+ }
222
+ // domcontentloaded, not networkidle2: an app that streams track data may
223
+ // never go idle, and the session gate below is a far better "it is up"
224
+ // signal than the absence of requests.
225
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout });
226
+ const report = await waitForJBrowseReady(page, {
227
+ // Spread, not a hand-copied field list. `OpenOptions extends
228
+ // ReadyOptions`, so every present and future ready option arrives here by
229
+ // construction. Listing them by hand is what dropped `allowUnsettled`:
230
+ // it was declared, documented, recommended by this function's own timeout
231
+ // message, and silently never forwarded, so `--allowUnsettled` did
232
+ // nothing and a timing-out stage always threw. Only the two options
233
+ // derived from the URL are overridden below.
234
+ ...options,
235
+ // A session spec's own assembly wins over the hub name. `--hub hg38
236
+ // --session spec.json` where the spec opens something else is legitimate
237
+ // (the hub is just supplying the config), and expecting the hub name there
238
+ // would fail a capture that is entirely correct.
239
+ assembly: urlOptions.assembly ??
240
+ (urlOptions.session
241
+ ? assemblyFromSession(urlOptions.session)
242
+ : undefined) ??
243
+ (urlOptions.session ? undefined : urlOptions.hub),
244
+ trackIds: trackIds ??
245
+ urlOptions.tracks ??
246
+ (urlOptions.session
247
+ ? trackIdsFromSession(urlOptions.session)
248
+ : undefined),
249
+ });
250
+ return { browser, page, url, ...report };
251
+ }
252
+ catch (error) {
253
+ // A launch that got as far as a page and then failed still holds a Chrome
254
+ // process; without this the caller has no handle to close it.
255
+ await browser.close();
256
+ throw error;
257
+ }
258
+ }
259
+ /**
260
+ * Open a JBrowse session, wait for it to render, screenshot it, close the
261
+ * browser. The one-call form.
262
+ */
263
+ export async function captureJBrowse(options = {}) {
264
+ const { out, fullPage = false, ...openOptions } = options;
265
+ const { browser, page, url, ...report } = await openJBrowse(openOptions);
266
+ try {
267
+ const image = await page.screenshot({ path: out, fullPage });
268
+ return { url, image, ...report };
269
+ }
270
+ finally {
271
+ await browser.close();
272
+ }
273
+ }
package/esm/hub.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ export declare function hubUrl(hub: string): string;
2
+ interface HubTrack {
3
+ trackId: string;
4
+ type?: string;
5
+ name?: string;
6
+ }
7
+ interface HubConfig {
8
+ tracks?: HubTrack[];
9
+ [key: string]: unknown;
10
+ }
11
+ /**
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.
15
+ */
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[]>;
23
+ export {};
package/esm/hub.js ADDED
@@ -0,0 +1,43 @@
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.
12
+ export function hubUrl(hub) {
13
+ const genark = /^(GC[AF])_(\d{3})(\d{3})(\d{3})/.exec(hub);
14
+ return genark
15
+ ? `${HUB_HOST}/hubs/genark/${genark[1]}/${genark[2]}/${genark[3]}/${genark[4]}/${hub}/config.json`
16
+ : `${HUB_HOST}/ucsc/${hub}/config.json`;
17
+ }
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);
26
+ 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.');
29
+ }
30
+ return (await res.json());
31
+ }
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.
36
+ */
37
+ 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;
43
+ }
package/esm/index.d.ts ADDED
@@ -0,0 +1,10 @@
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';
10
+ export type { JBrowseUrlOptions } from './url.ts';
package/esm/index.js ADDED
@@ -0,0 +1,7 @@
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';
@@ -0,0 +1,25 @@
1
+ export declare function sessionSpecParam(session: object): string;
2
+ export declare function encodeSessionSpec(session: object): string;
3
+ /**
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.
10
+ */
11
+ export declare function assemblyFromSession(session: object): string | undefined;
12
+ /**
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.
19
+ */
20
+ export declare function trackIdsFromSession(session: object): string[];
21
+ export declare function sessionSpecQuery({ config, session, sessionName, }: {
22
+ config: string;
23
+ session: object;
24
+ sessionName?: string;
25
+ }): string;
package/esm/session.js ADDED
@@ -0,0 +1,61 @@
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.
6
+ export function sessionSpecParam(session) {
7
+ return `spec-${JSON.stringify(session)}`;
8
+ }
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.
14
+ export function encodeSessionSpec(session) {
15
+ return encodeURIComponent(sessionSpecParam(session));
16
+ }
17
+ /**
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.
24
+ */
25
+ export function assemblyFromSession(session) {
26
+ const find = (views) => {
27
+ for (const view of views ?? []) {
28
+ const found = view.assembly ?? find(view.views);
29
+ if (found) {
30
+ return found;
31
+ }
32
+ }
33
+ return undefined;
34
+ };
35
+ return find(session.views);
36
+ }
37
+ /**
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.
44
+ */
45
+ export function trackIdsFromSession(session) {
46
+ 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),
53
+ ...collect(view.views),
54
+ ]);
55
+ return collect(session.views);
56
+ }
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).
59
+ export function sessionSpecQuery({ config, session, sessionName = 'Screenshot', }) {
60
+ return `?config=${config}&session=${encodeSessionSpec(session)}&sessionName=${encodeURIComponent(sessionName)}`;
61
+ }