overleaf/services/web/frontend/js/features/pdf-preview/util/pdf-caching-transport.js

144 lines
4.3 KiB
JavaScript
Raw Normal View History

import OError from '@overleaf/o-error'
import { fallbackRequest, fetchRange } from './pdf-caching'
import { captureException } from '../../../infrastructure/error-reporter'
import { getPdfCachingMetrics } from './metrics'
import {
cachedUrlLookupEnabled,
enablePdfCaching,
prefetchingEnabled,
prefetchLargeEnabled,
trackPdfDownloadEnabled,
} from './pdf-caching-flags'
export function generatePdfCachingTransportFactory(PDFJS) {
// NOTE: The custom transport can be used for tracking download volume.
if (!enablePdfCaching && !trackPdfDownloadEnabled) {
return () => null
}
const usageScore = new Map()
const cachedUrls = new Map()
const metrics = Object.assign(getPdfCachingMetrics(), {
failedCount: 0,
failedOnce: false,
tooMuchBandwidthCount: 0,
tooManyRequestsCount: 0,
cachedCount: 0,
cachedBytes: 0,
fetchedCount: 0,
fetchedBytes: 0,
latencyComputeMax: 0,
latencyComputeTotal: 0,
requestedCount: 0,
requestedBytes: 0,
oldUrlHitCount: 0,
oldUrlMissCount: 0,
enablePdfCaching,
prefetchingEnabled,
prefetchLargeEnabled,
cachedUrlLookupEnabled,
})
const verifyChunks =
new URLSearchParams(window.location.search).get('verify_chunks') === 'true'
class PDFDataRangeTransport extends PDFJS.PDFDataRangeTransport {
constructor({ url, pdfFile, abortController, handleFetchError }) {
super(pdfFile.size, new Uint8Array())
this.url = url
this.pdfFile = pdfFile
this.handleFetchError = handleFetchError
this.abortController = abortController
}
abort() {
this.abortController.abort()
}
requestDataRange(start, end) {
const abortSignal = this.abortController.signal
const errorInfo = {
pdfFile: this.pdfFile,
pdfUrl: this.url,
start,
end,
metrics,
}
const is404onOutputPDF = err =>
err.message === 'non successful response status: 404' &&
OError.getFullInfo(err).url === this.url
fetchRange({
url: this.url,
start,
end,
file: this.pdfFile,
metrics,
usageScore,
cachedUrls,
verifyChunks,
prefetchingEnabled,
prefetchLargeEnabled,
cachedUrlLookupEnabled,
abortSignal,
})
.catch(err => {
if (abortSignal.aborted) return
if (is404onOutputPDF(err)) {
// Do not consider a 404 on the main pdf url as pdf caching failure.
throw new PDFJS.MissingPDFException()
}
metrics.failedCount++
metrics.failedOnce = true
if (!enablePdfCaching) {
throw err // This was a fallback request already. Do not retry.
}
err = OError.tag(err, 'optimized pdf download error', errorInfo)
console.error(err)
captureException(err, { tags: { fromPdfCaching: true } })
return fallbackRequest({
url: this.url,
start,
end,
abortSignal,
}).catch(err => {
if (is404onOutputPDF(err)) {
// Do not consider a 404 on the main pdf url as pdf caching failure.
err = new PDFJS.MissingPDFException()
}
throw err
})
})
.then(blob => {
if (abortSignal.aborted) return
this.onDataRange(start, blob)
})
.catch(err => {
if (abortSignal.aborted) return
err = OError.tag(err, 'fatal pdf download error', errorInfo)
console.error(err)
if (!(err instanceof PDFJS.MissingPDFException)) {
captureException(err, { tags: { fromPdfCaching: true } })
}
// Signal error for (subsequent) page load.
this.handleFetchError(err)
})
}
}
return function ({ url, pdfFile, abortController, handleFetchError }) {
if (metrics.failedOnce) {
// Disable pdf caching once any fetch request failed.
// Be trigger-happy here until we reached a stable state of the feature.
return null
}
// Latency is collected per preview cycle.
metrics.latencyComputeMax = 0
metrics.latencyComputeTotal = 0
return new PDFDataRangeTransport({
url,
pdfFile,
abortController,
handleFetchError,
})
}
}