UNPKG

@jsreport/jsreport-chrome-pdf

Version:

jsreport recipe which is rendering pdf from html using headless chrome

207 lines (170 loc) 5.74 kB
const conversion = require('./conversion') const url = require('url') module.exports = ({ reporter, puppeteer, options }) => { const pool = [] const tasksQueue = [] const { numberOfWorkers } = options if (numberOfWorkers < 1) { throw new Error('"numberOfWorkers" must be a number greater or equal than 1') } const execute = async ({ strategy, launchOptions, conversionOptions, req, imageExecution, allowLocalFilesAccess, onOutput, res }) => { let browserInfo let page let releaseBrowser let crashError = false let timeoutError = false let htmlUrl if (conversionOptions.url) { htmlUrl = conversionOptions.url } else { const { pathToFile } = await res.output.writeToTempFile((uuid) => `${uuid}-${imageExecution ? 'chrome-image' : 'chrome-pdf'}.html`) htmlUrl = url.pathToFileURL(pathToFile) } try { const result = await conversion({ reporter, getBrowser: async () => { const allocation = await allocateBrowser( puppeteer, pool, tasksQueue, { numberOfWorkers, launchOptions } ) browserInfo = allocation.browserInfo releaseBrowser = allocation.releaseBrowser return browserInfo.instance }, htmlUrl, strategy, req, timeout: reporter.getReportTimeout(req), allowLocalFilesAccess, imageExecution, options: conversionOptions }) page = result.page const output = { type: result.type, content: result.content } if (onOutput) { await onOutput(output) delete output.content } return output } catch (err) { if (err.workerCrashed) { crashError = true } else if (err.workerTimeout) { timeoutError = true } throw err } finally { if (releaseBrowser) { releaseBrowser() } if ((crashError || timeoutError) && browserInfo) { recycleBrowser(puppeteer, browserInfo, launchOptions).catch(() => {}) } else if (page && !page.isClosed()) { await page.close() } tryFlushTasksQueue(puppeteer, pool, tasksQueue) } } execute.kill = async () => { const op = [] pool.forEach(async (browserInfo) => { if (browserInfo.recycling) { op.push(browserInfo.recycling.then(async () => { if (browserInfo.instance) { return browserInfo.instance.pages().then(pages => Promise.all(pages.map(page => page.close()))).then(() => browserInfo.instance.close()) } })) } else if (browserInfo.instance) { op.push(browserInfo.instance.pages().then(pages => Promise.all(pages.map(page => page.close()))).then(() => browserInfo.instance.close())) } }) return Promise.all(op) } return execute } async function createBrowser (puppeteer, browserInfo, launchOptions) { browserInfo.instance = await puppeteer.launch(launchOptions) browserInfo.instance.on('disconnected', () => { // clean references when the browser is no longer active (it has crashed or closed) browserInfo.instance = null }) } async function allocateBrowser (puppeteer, pool, tasksQueue, options) { const { numberOfWorkers, launchOptions } = options let browserInfo if (pool.length < numberOfWorkers) { browserInfo = { instance: undefined, isBusy: true } pool.push(browserInfo) await createBrowser(puppeteer, browserInfo, launchOptions) } else { // simple round robin balancer across browsers, // get the first available browser from the list const availableBrowserIndex = pool.findIndex((b) => b.isBusy === false) if (availableBrowserIndex !== -1) { browserInfo = pool.splice(availableBrowserIndex, 1)[0] browserInfo.isBusy = true // ..and then if browser found then make it the last item in the list // to continue the rotation pool.push(browserInfo) // we check that instance exists, in theory there will be always an instance, // however there is small chance that an error while recycling a chrome instance happens // and we end with .instance being null, in which case we need to create it here if (browserInfo.instance == null) { await createBrowser(puppeteer, browserInfo, launchOptions) } } else { return new Promise((resolve, reject) => { tasksQueue.push({ resolve, reject, options }) }) } } return { browserInfo, releaseBrowser: () => { browserInfo.isBusy = false } } } async function recycleBrowser (puppeteer, browserInfo, launchOptions) { browserInfo.isBusy = true let resolveRecycling browserInfo.recycling = new Promise((resolve) => { resolveRecycling = resolve }) if (browserInfo.instance) { try { const pages = await browserInfo.instance.pages() await Promise.all(pages.map(page => page.close())) } finally { await browserInfo.instance.close() } } // clean the property before trying to get new instance, this let us // create the instance later if for some reason the instance can not be // created during recycling browserInfo.instance = null try { await createBrowser(puppeteer, browserInfo, launchOptions) } finally { if (resolveRecycling) { resolveRecycling() } browserInfo.isBusy = false } } function tryFlushTasksQueue (puppeteer, pool, tasksQueue) { if (tasksQueue.length === 0) { return } const task = tasksQueue.shift() allocateBrowser(puppeteer, pool, tasksQueue, task.options).then(task.resolve).catch(task.reject) }