@jbrowse/capture 5.0.0-beta.3 → 5.0.0-beta.7
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 +38 -40
- package/esm/args.d.ts +14 -4
- package/esm/args.js +78 -84
- package/esm/bin.js +17 -15
- package/esm/capture.js +21 -7
- package/esm/index.d.ts +2 -2
- package/esm/index.js +1 -1
- package/esm/ready.d.ts +19 -41
- package/esm/ready.js +57 -191
- package/esm/session.js +13 -3
- package/esm/sessionGate.d.ts +20 -66
- package/esm/sessionGate.js +44 -148
- package/esm/waits.d.ts +24 -46
- package/esm/waits.js +39 -62
- package/package.json +2 -2
package/esm/sessionGate.js
CHANGED
|
@@ -1,73 +1,46 @@
|
|
|
1
|
-
// `window.JBrowseSession` is jbrowse-web's own devtools/automation handle
|
|
2
|
-
// (products/jbrowse-web/src/components/JBrowse.tsx). It is declared `unknown`
|
|
3
|
-
// there and is a live MST node here, so a cast at the page boundary is
|
|
4
|
-
// unavoidable. It is repeated in each function below rather than shared: every
|
|
5
|
-
// one of them is serialized into the page by puppeteer, so it can only call what
|
|
6
|
-
// it declares inside itself.
|
|
7
1
|
/**
|
|
8
2
|
* The census the app publishes beside its phase: `AppReadyMarker` renders
|
|
9
3
|
* `data-app-views` (a count), and `data-app-assemblies` / `data-app-tracks`
|
|
10
|
-
* (JSON string arrays of what is open).
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
4
|
+
* (JSON string arrays of what is open).
|
|
5
|
+
*
|
|
6
|
+
* One element answers the whole gate. Each view declares what it holds and
|
|
7
|
+
* `openViews`/`openTracks` reduce over those declarations (ADR-103), so nothing
|
|
8
|
+
* here has to know that a synteny view keeps its tracks on levels and its rows
|
|
9
|
+
* on sub-views — the duck-typed walk of `window.JBrowseSession` that used to
|
|
10
|
+
* live here, and drifted against the copy in `isPageBusyInPage`, is gone with
|
|
11
|
+
* the builds that needed it.
|
|
12
|
+
*
|
|
15
13
|
* `scripts/readinessContract.test.ts` pins the attribute names against the
|
|
16
14
|
* marker, since neither package may import the other.
|
|
17
15
|
*/
|
|
18
16
|
export const APP_CENSUS = '[data-app-tracks]';
|
|
19
|
-
/** What the page currently has open, or undefined if there is no
|
|
17
|
+
/** What the page currently has open, or undefined if there is no census yet. */
|
|
20
18
|
export function readSessionSummary(page) {
|
|
21
19
|
return page.evaluate(readSessionSummaryInPage);
|
|
22
20
|
}
|
|
23
|
-
// Serialized into the page, so it can only call what it declares
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// so calling it in node against a stubbed global exercises the very function
|
|
28
|
-
// puppeteer serializes, rather than a copy of it that can drift.
|
|
21
|
+
// Serialized into the page, so it can only call what it declares. Exported for
|
|
22
|
+
// its test: it reads the document and nothing else, so calling it in node
|
|
23
|
+
// against a staged DOM exercises the very function puppeteer serializes, rather
|
|
24
|
+
// than a copy of it that can drift.
|
|
29
25
|
export function readSessionSummaryInPage() {
|
|
30
|
-
// census first: one element, no walk (see APP_CENSUS)
|
|
31
26
|
const marker = document.querySelector('[data-app-tracks]');
|
|
32
|
-
if (marker) {
|
|
33
|
-
try {
|
|
34
|
-
return {
|
|
35
|
-
views: Number(marker.dataset.appViews) || 0,
|
|
36
|
-
assemblies: JSON.parse(marker.dataset.appAssemblies ?? '[]'),
|
|
37
|
-
trackIds: JSON.parse(marker.dataset.appTracks ?? '[]'),
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
// a malformed census falls through to the walk rather than failing the read
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
const session = globalThis.JBrowseSession;
|
|
45
|
-
const views = session?.views;
|
|
46
|
-
if (!views) {
|
|
27
|
+
if (!marker) {
|
|
47
28
|
return undefined;
|
|
48
29
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
30
|
+
try {
|
|
31
|
+
return {
|
|
32
|
+
views: Number(marker.dataset.appViews) || 0,
|
|
33
|
+
assemblies: JSON.parse(marker.dataset.appAssemblies ?? '[]'),
|
|
34
|
+
trackIds: JSON.parse(marker.dataset.appTracks ?? '[]'),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
57
39
|
}
|
|
58
|
-
return {
|
|
59
|
-
views: views.length,
|
|
60
|
-
assemblies: [
|
|
61
|
-
...new Set(views.flatMap(function asm(v) {
|
|
62
|
-
return [...(v.assemblyNames ?? []), ...(v.views ?? []).flatMap(asm)];
|
|
63
|
-
})),
|
|
64
|
-
],
|
|
65
|
-
trackIds: views.flatMap(v => collect(v).map(t => t.configuration?.trackId ?? '(unnamed)')),
|
|
66
|
-
};
|
|
67
40
|
}
|
|
68
41
|
/**
|
|
69
|
-
* Wait until the
|
|
70
|
-
*
|
|
42
|
+
* Wait until the census exists and the assembly and tracks that were asked for
|
|
43
|
+
* are actually open.
|
|
71
44
|
*
|
|
72
45
|
* Throws on timeout rather than proceeding. A config URL that 404s, a trackId
|
|
73
46
|
* that does not exist in the config, and an assembly name that does not match
|
|
@@ -79,53 +52,21 @@ export async function waitForSession(page, { assembly, trackIds = [], timeout =
|
|
|
79
52
|
try {
|
|
80
53
|
// #region session-gate
|
|
81
54
|
await page.waitForFunction((wantAssembly, wantTracks) => {
|
|
82
|
-
// The census the app publishes on its ready marker (see APP_CENSUS):
|
|
83
|
-
// on a build that has it, the whole gate is a read of one element.
|
|
84
55
|
const marker = document.querySelector('[data-app-tracks]');
|
|
85
|
-
if (marker) {
|
|
86
|
-
try {
|
|
87
|
-
const openViews = Number(marker.dataset.appViews);
|
|
88
|
-
const assemblies = JSON.parse(marker.dataset.appAssemblies ?? '[]');
|
|
89
|
-
const openTracks = JSON.parse(marker.dataset.appTracks ?? '[]');
|
|
90
|
-
return (openViews > 0 &&
|
|
91
|
-
(wantAssembly === null || assemblies.includes(wantAssembly)) &&
|
|
92
|
-
wantTracks.every(id => openTracks.includes(id)));
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
// No census: a deployed build older than the marker's attributes, so
|
|
99
|
-
// walk the session model it publishes instead.
|
|
100
|
-
const session = globalThis.JBrowseSession;
|
|
101
|
-
const views = session?.views;
|
|
102
|
-
if (!views?.length) {
|
|
56
|
+
if (!marker) {
|
|
103
57
|
return false;
|
|
104
58
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return
|
|
59
|
+
try {
|
|
60
|
+
const openViews = Number(marker.dataset.appViews) || 0;
|
|
61
|
+
const assemblies = JSON.parse(marker.dataset.appAssemblies ?? '[]');
|
|
62
|
+
const openTracks = JSON.parse(marker.dataset.appTracks ?? '[]');
|
|
63
|
+
return (openViews > 0 &&
|
|
64
|
+
(wantAssembly === null || assemblies.includes(wantAssembly)) &&
|
|
65
|
+
wantTracks.every(id => openTracks.includes(id)));
|
|
110
66
|
}
|
|
111
|
-
|
|
112
|
-
// and its tracks on the levels, so both walks descend into sub-views
|
|
113
|
-
// and levels rather than reading the top view only.
|
|
114
|
-
const asmOf = (v) => [
|
|
115
|
-
...(v.assemblyNames ?? []),
|
|
116
|
-
...(v.views ?? []).flatMap(asmOf),
|
|
117
|
-
];
|
|
118
|
-
if (wantAssembly !== null &&
|
|
119
|
-
!views.some(v => asmOf(v).includes(wantAssembly))) {
|
|
67
|
+
catch {
|
|
120
68
|
return false;
|
|
121
69
|
}
|
|
122
|
-
const tracksOf = (v) => [
|
|
123
|
-
...(v.tracks ?? []),
|
|
124
|
-
...(v.trackContainers ?? v.levels ?? []).flatMap(l => l.tracks ?? []),
|
|
125
|
-
...(v.views ?? []).flatMap(tracksOf),
|
|
126
|
-
];
|
|
127
|
-
const open = new Set(views.flatMap(v => tracksOf(v).map(t => t.configuration?.trackId)));
|
|
128
|
-
return wantTracks.every(id => open.has(id));
|
|
129
70
|
}, { timeout, polling: 250 }, assembly ?? null, trackIds);
|
|
130
71
|
// #endregion session-gate
|
|
131
72
|
}
|
|
@@ -136,7 +77,9 @@ export async function waitForSession(page, { assembly, trackIds = [], timeout =
|
|
|
136
77
|
const summary = await readSessionSummary(page).catch(() => undefined);
|
|
137
78
|
const found = summary
|
|
138
79
|
? `${summary.views} view(s), assemblies [${summary.assemblies.join(', ')}], tracks [${summary.trackIds.join(', ')}]`
|
|
139
|
-
: 'no
|
|
80
|
+
: 'no census on the page at all — either this is not a JBrowse app, or ' +
|
|
81
|
+
'it predates the readiness marker, in which case point --instance at a ' +
|
|
82
|
+
'build that publishes one';
|
|
140
83
|
const missing = summary
|
|
141
84
|
? trackIds.filter(id => !summary.trackIds.includes(id))
|
|
142
85
|
: trackIds;
|
|
@@ -147,61 +90,16 @@ export async function waitForSession(page, { assembly, trackIds = [], timeout =
|
|
|
147
90
|
.filter(Boolean)
|
|
148
91
|
.join(' and ');
|
|
149
92
|
throw new Error(`the session never reached the requested state after ${timeout}ms. ` +
|
|
150
|
-
`Wanted ${wanted || 'an
|
|
93
|
+
`Wanted ${wanted || 'an open view'}; found ${found}. ` +
|
|
151
94
|
'A config URL that 404s, a trackId the config does not define, or an ' +
|
|
152
95
|
'assembly name that does not match the config all look like this.');
|
|
153
96
|
}
|
|
154
97
|
}
|
|
155
|
-
export const PAINT_CONTRACT_NOTE = 'this JBrowse build publishes no data-display-drawn attributes, so "every ' +
|
|
156
|
-
'display has painted" cannot be checked — only that nothing is still ' +
|
|
157
|
-
'loading. Raise --settle if the image looks half-drawn.';
|
|
158
|
-
/**
|
|
159
|
-
* Whether every open display's paint state was actually measurable.
|
|
160
|
-
*
|
|
161
|
-
* The per-display paint attributes are not universal. Measured on 2026-08-07:
|
|
162
|
-
* jbrowse.org/code/jb2/main publishes `data-view-phase`, `data-display-phase`
|
|
163
|
-
* and `data-display-drawn`; jbrowse.org/code/jb2/latest — the released build,
|
|
164
|
-
* which is what every genomes.jbrowse.org link and every docs figure link opens
|
|
165
|
-
* — publishes none of them and exposes only the loading overlay. Against that
|
|
166
|
-
* instance the display-level waits are unfalsifiable rather than satisfied, so a
|
|
167
|
-
* caller has to be told which of the two it got instead of reading "0 displays
|
|
168
|
-
* pending" as good news.
|
|
169
|
-
*
|
|
170
|
-
* The absence of the attribute is NOT on its own the answer, which is the trap
|
|
171
|
-
* here: a page with no tracks open — an import form, a menu shot, a bare view —
|
|
172
|
-
* publishes none of it either, and reporting that as "this build cannot measure
|
|
173
|
-
* paint" is both wrong and alarming. So this asks the question only when there
|
|
174
|
-
* is something to measure, and answers true when there is not.
|
|
175
|
-
*
|
|
176
|
-
* Call it AFTER the session gate, so "no tracks open" means the page genuinely
|
|
177
|
-
* has none rather than not having got there yet.
|
|
178
|
-
*/
|
|
179
|
-
export async function hasPaintContract(page) {
|
|
180
|
-
const summary = await readSessionSummary(page);
|
|
181
|
-
if (summary && summary.trackIds.length === 0) {
|
|
182
|
-
return true;
|
|
183
|
-
}
|
|
184
|
-
return page.evaluate(() => document.querySelector('[data-display-drawn]') !== null);
|
|
185
|
-
}
|
|
186
|
-
/** Serialized into the page; exported so a test can call the real function. */
|
|
187
|
-
export function readInstrumentationInPage() {
|
|
188
|
-
return {
|
|
189
|
-
viewPhase: document.querySelector('[data-view-phase]') !== null,
|
|
190
|
-
displayPhase: document.querySelector('[data-display-phase]') !== null,
|
|
191
|
-
displayDrawn: document.querySelector('[data-display-drawn]') !== null,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
export function readInstrumentation(page) {
|
|
195
|
-
return page.evaluate(readInstrumentationInPage);
|
|
196
|
-
}
|
|
197
98
|
/**
|
|
198
99
|
* Displays that were still reporting unpainted at the moment of the call.
|
|
199
100
|
*
|
|
200
101
|
* Distinct from `unsettled`, which says a wait ran out of time. This says what
|
|
201
|
-
* the page looked like when the shutter fired
|
|
202
|
-
* other: a display can go back to pending after its stage passed. An empty
|
|
203
|
-
* result means nothing at all on a build with no paint contract — check
|
|
204
|
-
* `paintContract` before reading it as good news.
|
|
102
|
+
* the page looked like when the shutter fired.
|
|
205
103
|
*/
|
|
206
104
|
export async function pendingDisplays(page) {
|
|
207
105
|
return (await pendingDisplayStates(page)).map(d => d.name);
|
|
@@ -218,14 +116,12 @@ export async function pendingDisplays(page) {
|
|
|
218
116
|
* `ready` it says it is done and reports no paint. That is the display's
|
|
219
117
|
* bug, not the wait's, and it is the one a longer timeout will
|
|
220
118
|
* never fix.
|
|
221
|
-
* absent the element publishes no phase: a build older than the
|
|
222
|
-
* attribute, or a surface that never grew one.
|
|
223
119
|
*
|
|
224
120
|
* Serialized into the page, so a test can call the real function. Read fresh
|
|
225
|
-
* from the DOM at report time rather than from handles the waits held. A handle
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
* in 28c6ee6d90).
|
|
121
|
+
* from the DOM at report time rather than from handles the waits held. A handle
|
|
122
|
+
* to an element that has since re-rendered throws `Node is detached from
|
|
123
|
+
* document`, which is how the previous attempt at this turned four diagnosable
|
|
124
|
+
* timeouts into nine opaque puppeteer errors (reverted in 28c6ee6d90).
|
|
229
125
|
*/
|
|
230
126
|
export function pendingDisplayStatesInPage() {
|
|
231
127
|
return [
|
package/esm/waits.d.ts
CHANGED
|
@@ -115,11 +115,15 @@ export declare const BUSY_SELECTOR: string;
|
|
|
115
115
|
* Is the app doing anything right now?
|
|
116
116
|
*
|
|
117
117
|
* Serialized into the page, so it declares everything it uses and takes
|
|
118
|
-
* `BUSY_SELECTOR` as an argument rather than importing it.
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
118
|
+
* `BUSY_SELECTOR` as an argument rather than importing it. Every signal is a
|
|
119
|
+
* data attribute a component sets deliberately, so restyling or rewording the
|
|
120
|
+
* UI cannot move the answer.
|
|
121
|
+
*
|
|
122
|
+
* It used to also walk `window.JBrowseSession` for each display's own status
|
|
123
|
+
* message — the only PER-DISPLAY signal a build with no readiness attributes
|
|
124
|
+
* had. That walk was a duck-typed copy of the session gate's, drifted onto a
|
|
125
|
+
* different container spelling (ADR-103), and it went with the builds that
|
|
126
|
+
* needed it: `data-display-phase="loading"` covers the same fetch directly.
|
|
123
127
|
*
|
|
124
128
|
* Exported so a test can call the real function rather than a copy of it.
|
|
125
129
|
*/
|
|
@@ -127,49 +131,25 @@ export declare function isPageBusyInPage(busySelector: string): boolean;
|
|
|
127
131
|
/**
|
|
128
132
|
* Wait until the app has been idle for an unbroken stretch.
|
|
129
133
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* BEFORE the work starts. Measured on jbrowse.org/code/jb2/latest with two
|
|
136
|
-
* remote tracks: the session reports both tracks open at ~2.5s and the
|
|
137
|
-
* loading overlay does not go up until ~3.5s. Every absence-based gate passes
|
|
138
|
-
* during that second, over an app that has drawn nothing.
|
|
134
|
+
* Idle answers "is it working NOW"; a page is finished only if it stays that
|
|
135
|
+
* way, because a track that ends one fetch and starts the next is momentarily
|
|
136
|
+
* idle and a single-sample read takes that gap for the end. Requiring the idle
|
|
137
|
+
* to HOLD closes it.
|
|
139
138
|
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
139
|
+
* NOT a readiness gate on its own — idle is an absence, equally true of an app
|
|
140
|
+
* that has not started. `waitForAppSettled` is the gate; this is the tool for
|
|
141
|
+
* "nothing is in flight right now", which is what a probe or a benchmark wants
|
|
142
|
+
* between two measured actions.
|
|
142
143
|
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* in-page timers and rAF once the tab is not visible, which is the state a
|
|
146
|
-
* headless capture sits in.
|
|
144
|
+
* Polled from node rather than in-page: chrome throttles in-page timers and rAF
|
|
145
|
+
* once the tab is not visible, which is the state a headless capture sits in.
|
|
147
146
|
*
|
|
148
|
-
* Returns false on timeout rather than throwing, like its neighbours
|
|
149
|
-
* caller decides whether a page that never went quiet is a failure or a slow
|
|
150
|
-
* page worth capturing anyway.
|
|
147
|
+
* Returns false on timeout rather than throwing, like its neighbours.
|
|
151
148
|
*/
|
|
152
|
-
export declare function waitForQuietPeriod(page: Page, { quietMs, timeout, pollMs,
|
|
149
|
+
export declare function waitForQuietPeriod(page: Page, { quietMs, timeout, pollMs, }?: {
|
|
153
150
|
quietMs?: number;
|
|
154
151
|
timeout?: number;
|
|
155
152
|
pollMs?: number;
|
|
156
|
-
/**
|
|
157
|
-
* Wait for the app to be seen BUSY before any idle counts, giving up on
|
|
158
|
-
* that after this long. Zero (the default) accepts idle immediately.
|
|
159
|
-
*
|
|
160
|
-
* This is what turns the wait positive. Idle is still an absence, so an app
|
|
161
|
-
* that has not begun looks exactly like one that has finished — measured on
|
|
162
|
-
* jbrowse.org/code/jb2/latest, where a chain starting at the session gate
|
|
163
|
-
* saw an idle page at 1.6s and the tracks did not draw until past 10s.
|
|
164
|
-
* Seeing the transition INTO work and back out of it is an observation of
|
|
165
|
-
* the work itself.
|
|
166
|
-
*
|
|
167
|
-
* The window is bounded because a page with nothing to fetch never goes
|
|
168
|
-
* busy at all, and hanging on that would be worse than the race it closes.
|
|
169
|
-
* Size it above the gap between the session gate passing and the first
|
|
170
|
-
* indicator appearing, measured at ~1s on that instance.
|
|
171
|
-
*/
|
|
172
|
-
busyWindowMs?: number;
|
|
173
153
|
}): Promise<boolean>;
|
|
174
154
|
/**
|
|
175
155
|
* Wait for the FIRST frame the app says it has finished.
|
|
@@ -180,8 +160,7 @@ export declare function waitForQuietPeriod(page: Page, { quietMs, timeout, pollM
|
|
|
180
160
|
* debounced next one starting. `waitForAppSettled` requires it to HOLD, and is
|
|
181
161
|
* what both the chain here and `jb.waitReady` use.
|
|
182
162
|
*
|
|
183
|
-
* Returns false if
|
|
184
|
-
* ready yet" from "a build too old to have the marker" and falls back.
|
|
163
|
+
* Returns false if it never appears within the timeout.
|
|
185
164
|
*/
|
|
186
165
|
export declare function waitForAppReady(page: Page, { timeout }?: {
|
|
187
166
|
timeout?: number;
|
|
@@ -216,9 +195,8 @@ export declare function hasAppReadyMarker(page: Page): Promise<boolean>;
|
|
|
216
195
|
* `waitForQuietPeriod` is: chrome throttles in-page timers and rAF once the tab
|
|
217
196
|
* is not visible, which is exactly the state a headless capture sits in.
|
|
218
197
|
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
* sleep for this ends up capturing the frame the sleep was there to avoid.
|
|
198
|
+
* Throws on a build too old for the marker rather than falling back — see the
|
|
199
|
+
* body.
|
|
222
200
|
*/
|
|
223
201
|
export declare function waitForAppSettled(page: Page, { timeout, holdMs, pollMs, }?: {
|
|
224
202
|
timeout?: number;
|
package/esm/waits.js
CHANGED
|
@@ -55,7 +55,7 @@ const APP_SETTLED_HOLD_MS = 1000;
|
|
|
55
55
|
* finished, and there is no content behind it to fall through to.
|
|
56
56
|
*/
|
|
57
57
|
export async function waitForLoadingComplete(page, { timeout = 30000, waitForDownloads = false, } = {}) {
|
|
58
|
-
await page.waitForFunction((selector) => document.
|
|
58
|
+
await page.waitForFunction((selector) => document.querySelector(selector) === null, { timeout, polling: 'mutation' }, LOADING_OVERLAY);
|
|
59
59
|
// `quietMs: 0` because this is the "nothing is in flight right now" question;
|
|
60
60
|
// the one that needs the idle to hold is waitForQuietPeriod's own caller.
|
|
61
61
|
return waitForDownloads
|
|
@@ -257,7 +257,7 @@ export function waitForViewPhases(page, timeoutMs) {
|
|
|
257
257
|
* `data-view-phase` a view still resolving its assembly, newer builds
|
|
258
258
|
*/
|
|
259
259
|
export const BUSY_SELECTOR = [
|
|
260
|
-
|
|
260
|
+
LOADING_OVERLAY,
|
|
261
261
|
'[data-busy="true"]',
|
|
262
262
|
'[data-display-phase="loading"]',
|
|
263
263
|
'[data-view-phase="loading"]',
|
|
@@ -266,60 +266,42 @@ export const BUSY_SELECTOR = [
|
|
|
266
266
|
* Is the app doing anything right now?
|
|
267
267
|
*
|
|
268
268
|
* Serialized into the page, so it declares everything it uses and takes
|
|
269
|
-
* `BUSY_SELECTOR` as an argument rather than importing it.
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
269
|
+
* `BUSY_SELECTOR` as an argument rather than importing it. Every signal is a
|
|
270
|
+
* data attribute a component sets deliberately, so restyling or rewording the
|
|
271
|
+
* UI cannot move the answer.
|
|
272
|
+
*
|
|
273
|
+
* It used to also walk `window.JBrowseSession` for each display's own status
|
|
274
|
+
* message — the only PER-DISPLAY signal a build with no readiness attributes
|
|
275
|
+
* had. That walk was a duck-typed copy of the session gate's, drifted onto a
|
|
276
|
+
* different container spelling (ADR-103), and it went with the builds that
|
|
277
|
+
* needed it: `data-display-phase="loading"` covers the same fetch directly.
|
|
274
278
|
*
|
|
275
279
|
* Exported so a test can call the real function rather than a copy of it.
|
|
276
280
|
*/
|
|
277
281
|
export function isPageBusyInPage(busySelector) {
|
|
278
|
-
|
|
279
|
-
return true;
|
|
280
|
-
}
|
|
281
|
-
const session = globalThis
|
|
282
|
-
.JBrowseSession;
|
|
283
|
-
const tracksOf = (c) => (c.tracks ?? []).flatMap(t => t.displays ?? []);
|
|
284
|
-
const displaysOf = (v) => [
|
|
285
|
-
...tracksOf(v),
|
|
286
|
-
...(v.trackContainers ?? v.levels ?? []).flatMap(tracksOf),
|
|
287
|
-
...(v.views ?? []).flatMap(displaysOf),
|
|
288
|
-
];
|
|
289
|
-
return (session?.views ?? [])
|
|
290
|
-
.flatMap(displaysOf)
|
|
291
|
-
.some(d => (d.message ?? d.statusMessage ?? '').trim() !== '');
|
|
282
|
+
return document.querySelector(busySelector) !== null;
|
|
292
283
|
}
|
|
293
284
|
/**
|
|
294
285
|
* Wait until the app has been idle for an unbroken stretch.
|
|
295
286
|
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
* BEFORE the work starts. Measured on jbrowse.org/code/jb2/latest with two
|
|
302
|
-
* remote tracks: the session reports both tracks open at ~2.5s and the
|
|
303
|
-
* loading overlay does not go up until ~3.5s. Every absence-based gate passes
|
|
304
|
-
* during that second, over an app that has drawn nothing.
|
|
287
|
+
* Idle answers "is it working NOW"; a page is finished only if it stays that
|
|
288
|
+
* way, because a track that ends one fetch and starts the next is momentarily
|
|
289
|
+
* idle and a single-sample read takes that gap for the end. Requiring the idle
|
|
290
|
+
* to HOLD closes it.
|
|
305
291
|
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
292
|
+
* NOT a readiness gate on its own — idle is an absence, equally true of an app
|
|
293
|
+
* that has not started. `waitForAppSettled` is the gate; this is the tool for
|
|
294
|
+
* "nothing is in flight right now", which is what a probe or a benchmark wants
|
|
295
|
+
* between two measured actions.
|
|
308
296
|
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
* in-page timers and rAF once the tab is not visible, which is the state a
|
|
312
|
-
* headless capture sits in.
|
|
297
|
+
* Polled from node rather than in-page: chrome throttles in-page timers and rAF
|
|
298
|
+
* once the tab is not visible, which is the state a headless capture sits in.
|
|
313
299
|
*
|
|
314
|
-
* Returns false on timeout rather than throwing, like its neighbours
|
|
315
|
-
* caller decides whether a page that never went quiet is a failure or a slow
|
|
316
|
-
* page worth capturing anyway.
|
|
300
|
+
* Returns false on timeout rather than throwing, like its neighbours.
|
|
317
301
|
*/
|
|
318
|
-
export async function waitForQuietPeriod(page, { quietMs = 1500, timeout = 30000, pollMs = 250,
|
|
319
|
-
const
|
|
320
|
-
const deadline = start + timeout;
|
|
302
|
+
export async function waitForQuietPeriod(page, { quietMs = 1500, timeout = 30000, pollMs = 250, } = {}) {
|
|
303
|
+
const deadline = Date.now() + timeout;
|
|
321
304
|
let quietSince;
|
|
322
|
-
let seenBusy = busyWindowMs === 0;
|
|
323
305
|
while (Date.now() < deadline) {
|
|
324
306
|
// A page that navigates or closes under us fails the evaluate; treat that
|
|
325
307
|
// as busy and let the deadline decide, rather than reporting quiet.
|
|
@@ -328,22 +310,12 @@ export async function waitForQuietPeriod(page, { quietMs = 1500, timeout = 30000
|
|
|
328
310
|
.catch(() => true);
|
|
329
311
|
const now = Date.now();
|
|
330
312
|
if (busy) {
|
|
331
|
-
seenBusy = true;
|
|
332
313
|
quietSince = undefined;
|
|
333
314
|
}
|
|
334
315
|
else {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
// every sample in it was idle, since a failed evaluate reads as busy.
|
|
339
|
-
seenBusy = true;
|
|
340
|
-
quietSince = start;
|
|
341
|
-
}
|
|
342
|
-
if (seenBusy) {
|
|
343
|
-
quietSince ??= now;
|
|
344
|
-
if (now - quietSince >= quietMs) {
|
|
345
|
-
return true;
|
|
346
|
-
}
|
|
316
|
+
quietSince ??= now;
|
|
317
|
+
if (now - quietSince >= quietMs) {
|
|
318
|
+
return true;
|
|
347
319
|
}
|
|
348
320
|
}
|
|
349
321
|
await delay(pollMs);
|
|
@@ -359,8 +331,7 @@ export async function waitForQuietPeriod(page, { quietMs = 1500, timeout = 30000
|
|
|
359
331
|
* debounced next one starting. `waitForAppSettled` requires it to HOLD, and is
|
|
360
332
|
* what both the chain here and `jb.waitReady` use.
|
|
361
333
|
*
|
|
362
|
-
* Returns false if
|
|
363
|
-
* ready yet" from "a build too old to have the marker" and falls back.
|
|
334
|
+
* Returns false if it never appears within the timeout.
|
|
364
335
|
*/
|
|
365
336
|
export function waitForAppReady(page, { timeout = 30000 } = {}) {
|
|
366
337
|
return settled(page.waitForSelector(APP_READY, { timeout, visible: false }));
|
|
@@ -397,13 +368,19 @@ export function hasAppReadyMarker(page) {
|
|
|
397
368
|
* `waitForQuietPeriod` is: chrome throttles in-page timers and rAF once the tab
|
|
398
369
|
* is not visible, which is exactly the state a headless capture sits in.
|
|
399
370
|
*
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
* sleep for this ends up capturing the frame the sleep was there to avoid.
|
|
371
|
+
* Throws on a build too old for the marker rather than falling back — see the
|
|
372
|
+
* body.
|
|
403
373
|
*/
|
|
404
374
|
export async function waitForAppSettled(page, { timeout = 30000, holdMs = APP_SETTLED_HOLD_MS, pollMs = 250, } = {}) {
|
|
405
375
|
if (!(await hasAppReadyMarker(page))) {
|
|
406
|
-
|
|
376
|
+
// Throws rather than falling back to the quiet period. The fallback was a
|
|
377
|
+
// no-op that reported success on a build with no attributes at all, which
|
|
378
|
+
// is how a spec that dropped its fixed sleep for this ended up capturing
|
|
379
|
+
// the frame the sleep was there to avoid. A build without the marker needs
|
|
380
|
+
// a newer build, not a wait that cannot fail.
|
|
381
|
+
throw new Error('this page publishes no [data-app-phase], so there is nothing positive ' +
|
|
382
|
+
'to wait for — every other readiness attribute is an absence an app ' +
|
|
383
|
+
'that has not started also satisfies.');
|
|
407
384
|
}
|
|
408
385
|
const deadline = Date.now() + timeout;
|
|
409
386
|
let readySince;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jbrowse/capture",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.7",
|
|
4
4
|
"description": "Drive a JBrowse 2 instance with Puppeteer and screenshot it once it has actually finished rendering",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jbrowse",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"puppeteer": "^25.10.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@jbrowse/core": "^5.0.0-beta.
|
|
37
|
+
"@jbrowse/core": "^5.0.0-beta.7"
|
|
38
38
|
},
|
|
39
39
|
"publishConfig": {
|
|
40
40
|
"access": "public"
|