@browserless/pdf 13.6.2 → 13.6.3

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.
Files changed (2) hide show
  1. package/package.json +4 -4
  2. package/src/index.js +50 -8
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@browserless/pdf",
3
3
  "description": "Convert websites to high-quality PDFs with customizable margins, background printing, and optimized scaling.",
4
4
  "homepage": "https://browserless.js.org/#/?id=pdfurl-options",
5
- "version": "13.6.2",
5
+ "version": "13.6.3",
6
6
  "main": "src",
7
7
  "author": {
8
8
  "email": "hello@microlink.io",
@@ -30,8 +30,8 @@
30
30
  "website-to-pdf"
31
31
  ],
32
32
  "dependencies": {
33
- "@browserless/goto": "13.6.2",
34
- "@browserless/screenshot": "13.6.2",
33
+ "@browserless/goto": "13.6.3",
34
+ "@browserless/screenshot": "13.6.3",
35
35
  "@kikobeats/time-span": "~1.0.12",
36
36
  "debug-logfmt": "~1.4.10",
37
37
  "p-reflect": "~2.1.0",
@@ -47,5 +47,5 @@
47
47
  "test": "exit 0"
48
48
  },
49
49
  "license": "MIT",
50
- "gitHead": "7c7c6fee9e82050887d5499ed9dc3bac16c88c42"
50
+ "gitHead": "7bc1c12120186d999a5ddedbcad4e3b5b52fb8fb"
51
51
  }
package/src/index.js CHANGED
@@ -9,6 +9,7 @@ const {
9
9
  captureWithNavigationRetry,
10
10
  isWhiteScreenshot,
11
11
  waitForDomStability,
12
+ waitForReady,
12
13
  resolveWaitForDom,
13
14
  SCREENSHOT_DEFAULT_OPTS
14
15
  } = require('@browserless/screenshot')
@@ -21,6 +22,16 @@ const PDF_DEFAULT_OPTS = {
21
22
  waitUntil: 'auto'
22
23
  }
23
24
 
25
+ // Share of the action budget the readiness gate may consume in `auto` mode. The
26
+ // remainder is reserved for the blank-SPA screenshot poll to re-wait, so the
27
+ // gate can't starve the fallback while total prepare stays within one `timeout`.
28
+ const READY_BUDGET_RATIO = 0.5
29
+
30
+ // Minimum visible characters for the text fast path. Matches the counting cap
31
+ // in `waitForReady`'s snapshot, which stops walking text nodes once reached —
32
+ // raising this above the cap would make the text fast path unreachable.
33
+ const TEXT_PAINTED_MIN = 200
34
+
24
35
  const getMargin = unit => {
25
36
  if (!unit) return unit
26
37
  if (typeof unit === 'object') return unit
@@ -54,8 +65,9 @@ module.exports = ({ goto, ...gotoOpts } = {}) => {
54
65
  }
55
66
 
56
67
  // Navigate `page` to `url` and wait until it is ready to print: DOM stability
57
- // plus, in `auto` mode, a screenshot poll that re-waits while the first paint
58
- // is still blank.
68
+ // plus, in `auto` mode, a navigation-tolerant readiness gate. Only a page that
69
+ // settles still-blank falls back to the (expensive, navigation-fragile)
70
+ // screenshot poll.
59
71
  const prepare = async (page, url, opts = {}) => {
60
72
  const {
61
73
  margin,
@@ -89,11 +101,38 @@ module.exports = ({ goto, ...gotoOpts } = {}) => {
89
101
  async function waitUntilAuto (page) {
90
102
  await waitForDomStabilityResult(page)
91
103
  const timeout = goto.timeouts.action(rest.timeout)
104
+ // One action budget shared by the readiness gate and the screenshot poll,
105
+ // so worst-case prepare stays within a single `timeout` instead of one
106
+ // per stage.
107
+ const elapsed = timeSpan()
108
+
109
+ // Cheap, navigation-tolerant readiness — no screenshots. Resolves once the
110
+ // page is visually quiet (height stable, images decoded, load complete),
111
+ // absorbing the client-side re-navigation that makes a screenshot poll
112
+ // throw `Execution context was destroyed`. Capped at a share of the budget
113
+ // so a slow gate still leaves the blank-SPA poll room to re-wait.
114
+ const ready = await waitForReady(page, { timeout: Math.round(timeout * READY_BUDGET_RATIO) })
115
+ debug('ready', { ...ready, duration: elapsed() })
116
+
117
+ // Fast path: the page settled with real painted content in a document
118
+ // taller than the viewport — a visibly rendered image (not a tracking
119
+ // pixel), or enough visible text with webfonts loaded (a pending
120
+ // `font-display: block` font renders text invisible, exactly when a
121
+ // capture would be white) — so it can't be a blank shell: skip the
122
+ // screenshot poll. Height and viewport come from the same in-page
123
+ // snapshot (`page.viewport()` is null under `defaultViewport: null`),
124
+ // and an unknown viewport skips the fast path rather than dropping the
125
+ // taller-than-viewport guard. A gate that timed out never settled, so
126
+ // don't trust its partial snapshot: fall through to the blank check.
127
+ const painted = ready.painted > 0 || (ready.text >= TEXT_PAINTED_MIN && ready.fonts)
128
+ if (!ready.timedOut && painted && ready.viewport > 0 && ready.height > ready.viewport) return
129
+
130
+ // Otherwise fall back to the screenshot poll — re-wait while the first
131
+ // paint is still blank — to keep the blank-SPA protection. The page has
132
+ // already settled, so the first capture won't race a navigation, and a
133
+ // non-blank page exits after that single shot without an extra re-wait.
92
134
  let isWhite = false
93
135
  let retry = -1
94
-
95
- const timePdf = timeSpan()
96
-
97
136
  do {
98
137
  ++retry
99
138
  const screenshotTime = timeSpan()
@@ -105,14 +144,17 @@ module.exports = ({ goto, ...gotoOpts } = {}) => {
105
144
  type: 'jpeg',
106
145
  quality: 30
107
146
  }),
108
- { page, goto, timeout }
147
+ // The retry loop keeps its own clock, so hand it only what is left
148
+ // of the shared budget — a fresh full `timeout` here would let one
149
+ // navigation-racing capture double the worst-case prepare time.
150
+ { page, goto, timeout: Math.max(0, timeout - elapsed()) }
109
151
  )
110
152
  isWhite = await isWhiteScreenshot(screenshot)
111
153
  if (isWhite) await goto.waitUntilAuto(page, { timeout: rest.timeout })
112
154
  debug('retry', { waitUntil, isWhite, retry, duration: screenshotTime() })
113
- } while (isWhite && timePdf() < timeout)
155
+ } while (isWhite && elapsed() < timeout)
114
156
 
115
- debug({ waitUntil, isWhite, timeout, duration: require('pretty-ms')(timePdf()) })
157
+ debug({ waitUntil, isWhite, timeout, duration: require('pretty-ms')(elapsed()) })
116
158
  }
117
159
  }
118
160