browserless 13.12.5 → 13.13.1

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 (3) hide show
  1. package/index.d.ts +2 -2
  2. package/package.json +5 -5
  3. package/src/index.js +44 -11
package/index.d.ts CHANGED
@@ -31,7 +31,7 @@ export interface Context {
31
31
  respawn: () => void
32
32
  context: () => Promise<BrowserContext>
33
33
  browser: () => Promise<Browser>
34
- evaluate: <T>(fn: (page: Page, response?: HTTPResponse | undefined, error?: Error) => T | Promise<T>, gotoOpts?: GotoOptions) => Promise<T>
34
+ evaluate: <T>(fn: (page: Page, response?: HTTPResponse | undefined, error?: Error) => T | Promise<T>, gotoOpts?: GotoOptions & { keepPage?: boolean, preserveContext?: boolean }) => Promise<T>
35
35
  goto: (page: Page, opts: GotoOptions & { url: string }) => Promise<GotoResult>
36
36
  html: (url: string, opts?: GotoOptions) => Promise<string>
37
37
  page: (name?: string) => Promise<Page>
@@ -41,7 +41,7 @@ export interface Context {
41
41
  getDevice: (deviceName: string) => Viewport | undefined
42
42
  report: (opts?: { benchmark?: boolean }) => Promise<HardwareInfo>
43
43
  destroyContext: (opts?: { force?: boolean }) => Promise<void>
44
- withPage: <T>(fn: (page: Page, goto: unknown) => Promise<T>, opts?: { timeout?: number }) => Promise<T>
44
+ withPage: <T>(fn: (page: Page, goto: unknown) => Promise<T>, opts?: { timeout?: number, keepPage?: boolean, preserveContext?: boolean }) => Promise<T>
45
45
  }
46
46
 
47
47
  export interface GotoOptions {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "browserless",
3
3
  "description": "The headless Chrome/Chromium driver on top of Puppeteer. Take screenshots, generate PDFs, extract text and HTML with a production-ready API.",
4
4
  "homepage": "https://browserless.js.org",
5
- "version": "13.12.5",
5
+ "version": "13.13.1",
6
6
  "types": "index.d.ts",
7
7
  "main": "src/index.js",
8
8
  "author": {
@@ -36,9 +36,9 @@
36
36
  ],
37
37
  "dependencies": {
38
38
  "@browserless/errors": "13.9.23",
39
- "@browserless/goto": "13.12.5",
40
- "@browserless/pdf": "13.12.5",
41
- "@browserless/screenshot": "13.12.5",
39
+ "@browserless/goto": "13.13.1",
40
+ "@browserless/pdf": "13.13.1",
41
+ "@browserless/screenshot": "13.13.1",
42
42
  "debug-logfmt": "~1.4.15",
43
43
  "kill-process-group": "~1.0.16",
44
44
  "p-reflect": "~2.1.0",
@@ -69,5 +69,5 @@
69
69
  "timeout": "2m",
70
70
  "workerThreads": false
71
71
  },
72
- "gitHead": "0e35854cf760df379112c723c75376e9b25e98fc"
72
+ "gitHead": "9b15762cba78cc57788376048161db257e2a9c76"
73
73
  }
package/src/index.js CHANGED
@@ -110,7 +110,10 @@ module.exports = ({ timeout: globalTimeout = 30000, ...launchOpts } = {}) => {
110
110
  }
111
111
  }
112
112
 
113
- const withPage = (fn, { timeout: evaluateTimeout } = {}) => {
113
+ const withPage = (
114
+ fn,
115
+ { timeout: evaluateTimeout, keepPage = false, preserveContext = false } = {}
116
+ ) => {
114
117
  const name = fn.name || 'anonymous'
115
118
 
116
119
  return async (...args) => {
@@ -119,24 +122,39 @@ module.exports = ({ timeout: globalTimeout = 30000, ...launchOpts } = {}) => {
119
122
  async function run () {
120
123
  let page
121
124
  let closePageTimeout
125
+ let isRetained = false
122
126
 
123
- try {
124
- page = await createPage(name)
125
- closePageTimeout = setTimeout(() => {
127
+ const startCloseTimeout = () => {
128
+ const timer = setTimeout(() => {
126
129
  closePage(page, name).catch(error => {
127
130
  const { message, code, name } = ensureError(error)
128
131
  debug('closePage:timeout:error', { message, code, name })
129
132
  })
130
133
  }, timeout)
131
- if (typeof closePageTimeout.unref === 'function') closePageTimeout.unref()
134
+ if (typeof timer.unref === 'function') timer.unref()
135
+ return timer
136
+ }
137
+
138
+ try {
139
+ page = await createPage(name)
140
+ closePageTimeout = startCloseTimeout()
132
141
  const value = await fn(page, goto)(...args)
133
- await closePage(page, `${name}:success`)
142
+ // A timeout already rejected the caller, who never received this
143
+ // page. Retaining it would clear the in-call watchdog and arm a
144
+ // new one with nobody holding the page.
145
+ if (keepPage && !isRejected) isRetained = true
146
+ else await closePage(page, `${name}:success`)
134
147
  return value
135
148
  } catch (error) {
136
149
  await closePage(page, `${name}:error`)
137
150
  if (!isRejected) throw ensureError(error)
138
151
  } finally {
152
+ // The in-call watchdog ends with the call. A retained page gets a
153
+ // fresh window of this same timeout, measured from handover.
154
+ // The owner has to finish or close the page inside that window.
155
+ // Failed attempts already closed their page, so only the last one retains.
139
156
  if (closePageTimeout) clearTimeout(closePageTimeout)
157
+ if (isRetained) startCloseTimeout()
140
158
  }
141
159
  }
142
160
 
@@ -150,10 +168,20 @@ module.exports = ({ timeout: globalTimeout = 30000, ...launchOpts } = {}) => {
150
168
  const isRetryable =
151
169
  error.code === 'EBRWSRCONTEXTCONNRESET' || error.code === 'EPROTOCOL'
152
170
  if (!isRetryable) throw error
171
+ const { message, attemptNumber, retriesLeft } = error
172
+ // Every attempt builds its own page, so both codes are retryable
173
+ // inside the existing context: `EBRWSRCONTEXTCONNRESET` covers a
174
+ // closed page, a detached frame and a destroyed execution context,
175
+ // which a fresh page recovers from. Replacing the context is the
176
+ // only part a caller that owns it cannot afford, since that closes
177
+ // the pages it still holds. Skip that, not the retry.
178
+ if (preserveContext) {
179
+ debug('retry', { attemptNumber, retriesLeft, message, preserveContext })
180
+ return
181
+ }
153
182
  const previousContextPromise = _contextPromise
154
183
  _contextPromise = createBrowserContext(contextOpts)
155
184
  await pReflect(previousContextPromise.then(ctx => ctx.close()))
156
- const { message, attemptNumber, retriesLeft } = error
157
185
  debug('retry', { attemptNumber, retriesLeft, message })
158
186
  }
159
187
  })
@@ -167,11 +195,15 @@ module.exports = ({ timeout: globalTimeout = 30000, ...launchOpts } = {}) => {
167
195
  }
168
196
  }
169
197
 
170
- const evaluate = (fn, gotoOpts) =>
171
- withPage(
198
+ const evaluate = (fn, gotoOpts) => {
199
+ // Both belong to withPage, so they are pulled out rather than spread into
200
+ // the navigation options.
201
+ const { keepPage, preserveContext, ...navigateOpts } = gotoOpts ?? {}
202
+
203
+ return withPage(
172
204
  Object.defineProperty(
173
205
  (page, goto) => async (url, opts) => {
174
- const { response, error } = await goto(page, { url, ...gotoOpts, ...opts })
206
+ const { response, error } = await goto(page, { url, ...navigateOpts, ...opts })
175
207
  return fn(page, response, error)
176
208
  },
177
209
  'name',
@@ -180,8 +212,9 @@ module.exports = ({ timeout: globalTimeout = 30000, ...launchOpts } = {}) => {
180
212
  writable: false
181
213
  }
182
214
  ),
183
- gotoOpts
215
+ { ...navigateOpts, keepPage, preserveContext }
184
216
  )
217
+ }
185
218
 
186
219
  const destroyContext = async ({ force = false } = {}) => {
187
220
  if (force) isDestroyedForced = true