import { get } from 'lodash-es' import QuickLRU from 'quick-lru' import patterns from '../lib/patterns.js' import getMiniTocItems from '../lib/get-mini-toc-items.js' import Page from '../lib/page.js' import statsd from '../lib/statsd.js' import { isConnectionDropped } from './halt-on-dropped-connection.js' import { nextApp, nextHandleRequest } from './next.js' function cacheOnReq(fn, minSize = 1024, lruMaxSize = 1000) { const cache = new QuickLRU({ maxSize: lruMaxSize }) return async function (req) { const path = req.pagePath || req.path // Is the request for the GraphQL Explorer page? const isGraphQLExplorer = req.context.currentPathWithoutLanguage === '/graphql/overview/explorer' // Serve from the cache if possible const isCacheable = // Skip for HTTP methods other than GET req.method === 'GET' && // Skip for JSON debugging info requests !('json' in req.query) && // Skip for the GraphQL Explorer page !isGraphQLExplorer if (isCacheable && cache.has(path)) { return cache.get(path) } const result = await fn(req) if (result && isCacheable && result.length > minSize) { cache.set(path, result) } return result } } async function buildRenderedPage(req) { const { context } = req const { page } = context const path = req.pagePath || req.path const pageRenderTimed = statsd.asyncTimer(page.render, 'middleware.render_page', [`path:${path}`]) const renderedPage = await pageRenderTimed(context) // handle special-case prerendered GraphQL objects page if (path.endsWith('graphql/reference/objects')) { return renderedPage + context.graphql.prerenderedObjectsForCurrentVersion.html } // handle special-case prerendered GraphQL input objects page if (path.endsWith('graphql/reference/input-objects')) { return renderedPage + context.graphql.prerenderedInputObjectsForCurrentVersion.html } // handle special-case prerendered GraphQL mutations page if (path.endsWith('graphql/reference/mutations')) { return renderedPage + context.graphql.prerenderedMutationsForCurrentVersion.html } return renderedPage } async function buildMiniTocItems(req) { const { context } = req const { page } = context // get mini TOC items on articles if (!page.showMiniToc) { return } return getMiniTocItems(context.renderedPage, page.miniTocMaxHeadingLevel) } // The avergage size of buildRenderedPage() is about 22KB. // The median in 7KB. By only caching those larger than 10KB we avoid // putting too much into the cache. const wrapRenderedPage = cacheOnReq(buildRenderedPage, 10 * 1024) export default async function renderPage(req, res, next) { const { context } = req const { page } = context const path = req.pagePath || req.path // render a 404 page if (!page) { if (process.env.NODE_ENV !== 'test' && context.redirectNotFound) { console.error( `\nTried to redirect to ${context.redirectNotFound}, but that page was not found.\n` ) } return nextApp.render404(req, res) } // Just finish fast without all the details like Content-Length if (req.method === 'HEAD') { return res.status(200).end() } // Updating the Last-Modified header for substantive changes on a page for engineering // Docs Engineering Issue #945 if (page.effectiveDate) { // Note that if a page has an invalidate `effectiveDate` string value, // it would be caught prior to this usage and ultimately lead to // 500 error. res.setHeader('Last-Modified', new Date(page.effectiveDate).toUTCString()) } // collect URLs for variants of this page in all languages page.languageVariants = Page.getLanguageVariants(path) // Stop processing if the connection was already dropped if (isConnectionDropped(req, res)) return req.context.renderedPage = await wrapRenderedPage(req) req.context.miniTocItems = await buildMiniTocItems(req) // Stop processing if the connection was already dropped if (isConnectionDropped(req, res)) return // Create string for