Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix infinite sidebar loading in Firefox >=130 with PDFs #6541

Merged
merged 1 commit into from
Sep 9, 2024

Conversation

acelaya
Copy link
Contributor

@acelaya acelaya commented Sep 9, 2024

When using the bookmarklet in Firefox, we use the PDF.js version embedded on it.

Firefox 130 has released with a new version of PDF.js which removes the prop we used to use to know that the PDF was downloaded, cuasing the sidebar to stay in an infinite loading state. See the conversation in slack https://hypothes-is.slack.com/archives/C2BLQDKHA/p1725653987800389

This PR adds a fallback check to PDFViewerApplication.pdfDocument.getDownloadInfo(), which returns a promise when the PDF is downloaded, and is what used to be used to set the removed prop (see https://github.com/mozilla/pdf.js/pull/18463/files#diff-e24e4bd32090b0c400a70f02d1b844df95c9bbe62cd0b280f4397e9f4194fe44L1224), so it should be equivalent.

I did not remove the previous check, but combined both using the new one as a fallback. This will reduce potential regressions in older versions of PDF.js, Firefox, or when using other browsers. However, it seems to be possible to rely only on PDFViewerApplication.pdfDocument.getDownloadInfo(), as it has been around for several versions.

Testing steps

  1. Check out this branch
  2. Make sure you have a locally running version of via

This PR requires mostly sanity checking a few scenarios, being:

These three scenarios should be tested in Firefox 130, to verify it works now, but also Firefox <130 and other browsers (chrome and/or Safari) to verify there are no regressions for those.

@acelaya acelaya force-pushed the pdfjs-firefox-fix branch 2 times, most recently from 19e490a to a0fae12 Compare September 9, 2024 09:19
@@ -94,6 +94,7 @@ class FakePDFViewerApplication {
this.dispatchDOMEvents = domEvents;
this.initialized = false;
this.newFingerprintAPI = newFingerprintAPI;
this.downloadComplete = false;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was previously not initialized, relying on the "falsy" behavior of undefined.

Now we need to initialize to ensure the code branches that need to run are preserved.

Copy link

codecov bot commented Sep 9, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 99.43%. Comparing base (72117eb) to head (12830f4).
Report is 5 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #6541   +/-   ##
=======================================
  Coverage   99.43%   99.43%           
=======================================
  Files         270      270           
  Lines       10213    10219    +6     
  Branches     2416     2417    +1     
=======================================
+ Hits        10155    10161    +6     
  Misses         58       58           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@@ -70,9 +86,10 @@ export class PDFMetadata {
* @param app - The `PDFViewerApplication` global from PDF.js
*/
constructor(app: PDFViewerApplication) {
this._loaded = pdfViewerInitialized(app).then(() => {
this._loaded = pdfViewerInitialized(app).then(async () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not going to change that here, as it may have other implications, but I think that we could simply do this:

this._loaded = pdfViewerInitialized(app)
    .then(() => app.pdfDocument.getDownloadInfo())
    .then(() => app);

But we would probably have to drop support for some older versions of PDF.js

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But we would probably have to drop support for some older versions of PDF.js

We need to clarify the supported range of PDF.js versions. I think there are some existing code comments somewhere. The main considerations would be:

  1. The version of PDF.js used in Via and the current extension
  2. Versions of PDF.js used in the last N Firefox releases
  3. Which older versions of https://github.com/hypothesis/pdf.js-hypothes.is we want to support

Of these, (3) is the main potential source of old PDF.js versions still in use. We might need to do some metrics gathering in the client to find out how many requests are coming from old releases, and also add some kind of warning banner if the client is loaded in an old version that is unsupported.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/**
* @return A promise that is resolved when the document's data is loaded.
* It is resolved with an {Object} that contains the `length` property
* that indicates size of the PDF data in bytes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@acelaya acelaya marked this pull request as draft September 9, 2024 09:33
} = {},
) {
this.url = url;
this.documentInfo = undefined;
this.metadata = undefined;
this.pdfDocument = null;
this.pdfDocument = new FakePDFDocumentProxy();
Copy link
Contributor Author

@acelaya acelaya Sep 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need this to be set from the beginning now, so I changed it so that it can be created "empty", but exposes a finishLoading method that is invoked when the fake app's own finishLoading method is called.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible in PDF.js that pdfDocument is unset either temporarily or permanently when Hypothesis is loaded? Do we handle that gracefully?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to our type definitions, it is mandatory, but looking at the code, it can be null apparently https://github.com/mozilla/pdf.js/blob/f68310b7b1db39a934124acc90867a949da2d091/web/app.js#L971

Also, if you check that code, the prop is initialized only when load method is called.

I'll create an issue to try to reproduce what happens when it is not set and handle it properly.

@acelaya acelaya marked this pull request as ready for review September 9, 2024 10:14
Copy link
Member

@marcospri marcospri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't offer much insight on the code changes themselves but I followed the test instructions in firefox 130.0-2 and 129.0.2-1 and everything works as expected.

@acelaya acelaya merged commit 4b60a07 into main Sep 9, 2024
4 checks passed
@acelaya acelaya deleted the pdfjs-firefox-fix branch September 9, 2024 11:34
Copy link
Member

@robertknight robertknight left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes generally look good in the happy path case. I had a query about whether we gracefully handle cases where the PDF has failed to load, has not yet started loading (is this possible) or the viewer was loaded without specifying a PDF.

* For newer PDF.js versions we wait for
* `PDFViewerApplication.pdfDocument.getDownloadInfo()` to resolve.
*/
async function isPDFDownloaded(app: PDFViewerApplication): Promise<boolean> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function has type Promise<boolean> but it only ever returns true. An API like whenPDFDownloaded(app): Promise<void> would probably give a better cue to readers about how it behaves.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function has type Promise<boolean> but it only ever returns true

Hmm, I'm not sure if this is actually the case.

Previously we were relying in the downloadComplete prop, which could be true or false. Now it can also be undefined, but it can still be false I guess? 🤔

In that case, if the prop is set and is false, we would be falling back to the other logic that was already in place, which listens for documentloaded/documentload on the event bus.

Or am I missing something?

@@ -70,9 +86,10 @@ export class PDFMetadata {
* @param app - The `PDFViewerApplication` global from PDF.js
*/
constructor(app: PDFViewerApplication) {
this._loaded = pdfViewerInitialized(app).then(() => {
this._loaded = pdfViewerInitialized(app).then(async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But we would probably have to drop support for some older versions of PDF.js

We need to clarify the supported range of PDF.js versions. I think there are some existing code comments somewhere. The main considerations would be:

  1. The version of PDF.js used in Via and the current extension
  2. Versions of PDF.js used in the last N Firefox releases
  3. Which older versions of https://github.com/hypothesis/pdf.js-hypothes.is we want to support

Of these, (3) is the main potential source of old PDF.js versions still in use. We might need to do some metrics gathering in the client to find out how many requests are coming from old releases, and also add some kind of warning banner if the client is loaded in an old version that is unsupported.

@@ -1,6 +1,7 @@
import { delay } from '@hypothesis/frontend-testing';
import EventEmitter from 'tiny-emitter';

import { promiseWithResolvers } from '../../../shared/promise-with-resolvers';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could replace this with Promise.withResolvers going forwards.

} = {},
) {
this.url = url;
this.documentInfo = undefined;
this.metadata = undefined;
this.pdfDocument = null;
this.pdfDocument = new FakePDFDocumentProxy();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible in PDF.js that pdfDocument is unset either temporarily or permanently when Hypothesis is loaded? Do we handle that gracefully?

/**
* Indicates the download of the PDF has completed.
* This prop is not set in PDF.js >=4.5, in which case you should use
* `PDFViewerApplication.pdfDocument.getDownloadInfo()` instead.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A link to the commit that removed this property upstream would be useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants