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

Getting errors when using PDFs cached using a service worker. #1924

Open
4 tasks done
kainbryanjones opened this issue Dec 19, 2024 · 0 comments
Open
4 tasks done

Getting errors when using PDFs cached using a service worker. #1924

kainbryanjones opened this issue Dec 19, 2024 · 0 comments
Labels
bug Something isn't working

Comments

@kainbryanjones
Copy link

Before you start - checklist

  • I followed instructions in documentation written for my React-PDF version
  • I have checked if this bug is not already reported
  • I have checked if an issue is not listed in Known issues
  • If I have a problem with PDF rendering, I checked if my PDF renders properly in PDF.js demo

Description

I'm building an app that needs to work offline.

To handle this we cache PDFs that we need.

Initial load works just fine but when revisiting the PDF the PDF fails to load.

Steps to reproduce

  1. Set up a service worker to cache any pdfs (I've placed my source code)
  2. Load the PDF and display it in browser (I'm running my app in a WebView on Android)
  3. Close the page and reopen the PDF with the now cached PDF

Expected behavior

Loads just fine

Actual behavior

Getting an error "cannot read properties of null (reading sendWithPromise)".
I have seen other issues talking about this, but can't seem to find a proper fix.

Additional information

Find attached the below source code

The service worker source code

const CACHE_NAME = 'cache-worker-v1';
const ASSET_CACHE_NAME_LOG = (...log: Parameters<typeof console.log>) => console.log(CACHE_NAME, ...log)

type AssetCachingDetails = {
    id: string
}

//https://github.com/uuidjs/uuid/blob/main/src/regex.ts
const getIsUUID = (id: string) => {
    const regex = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/i
    return regex.test(id)
}

const getValidId = (index: number, pathname: string, shouldBeUUID = true): AssetCachingDetails | null => {
    ASSET_CACHE_NAME_LOG("getIsValidId", pathname)
    const split = pathname.split("/");
    const id = split.at(index);
    ASSET_CACHE_NAME_LOG("getIsValidId", `pathname: ${pathname}`, `id: ${id}`)
    if (id === undefined) return null;
    if (shouldBeUUID) {
        const isUUID = getIsUUID(id)
        ASSET_CACHE_NAME_LOG("getIsValidId", pathname, `shouldBeUUID: ${shouldBeUUID}`, `isUUID: ${isUUID}`)
        if (!isUUID) return null
    }
    return {id};
}

/**
 * Will determine the id of the data to be cached if it matches a url that we cache.
 *
 * We cache
 * GET /v2/app/info/recommended/{caseId}
 * GET /v2/app/info/{id}
 * GET /v2/app/info/asset/{orgId}/{assetId}/{filenameWithExt}
 *
 * @returns the correct id to pass to the cache or NULL if the url was invalid
 * @param pathname the pathname to check
 */
const getAssetCachingDetails = (pathname: string): AssetCachingDetails | null => {

    const recommendedPathnamePrefix = "/v2/app/info/recommended/"
    const infoCardPathnamePrefix = "/v2/app/info/"
    const assetPathnamePrefix = "/v2/app/info/asset/"

    const isRecommended = pathname.startsWith(recommendedPathnamePrefix)
    const isAsset = pathname.startsWith(assetPathnamePrefix)
    const isInfoCard = pathname.startsWith(infoCardPathnamePrefix) && !isAsset && !isRecommended


    let id: ReturnType<typeof getAssetCachingDetails> = null

    if (isRecommended) id = getValidId(5, pathname, false)
    else if (isAsset) id = getValidId(6, pathname)
    else if (isInfoCard) id = getValidId(4, pathname)

    return id
}

self.addEventListener("fetch", (ev) => {

    // @ts-expect-error the FetchEvent is untyped
    ASSET_CACHE_NAME_LOG(JSON.stringify(ev.request.url))

    // @ts-expect-error the FetchEvent is untyped
    const url = new URL(ev.request.url)
    ASSET_CACHE_NAME_LOG(`URL: ${url.toString()}`)
    ASSET_CACHE_NAME_LOG(`pathname: ${url.pathname}`)
    const assetCachingDetails = getAssetCachingDetails(url.pathname)

    ASSET_CACHE_NAME_LOG(url.pathname, JSON.stringify(assetCachingDetails === null ? "Not to be cached" : assetCachingDetails))
    if (assetCachingDetails === null) return;

    const {id} = assetCachingDetails;
    ASSET_CACHE_NAME_LOG(`Begin caching ${id} for url ${url.pathname}`)

    // @ts-expect-error the FetchEvent is untyped
    ev.respondWith(
        (async () => {
            const cache = await caches.open(CACHE_NAME);
            let networkResponse: Response
            try {
                /**
                 * We want to use the cached response from previous fetch requests IF this response fails
                 */
                // @ts-expect-error the FetchEvent is untyped
                networkResponse = await fetch(ev.request);

                if (!networkResponse.ok) throw Error()

                const clonedResponse = networkResponse.clone();
                ASSET_CACHE_NAME_LOG(`Caching the response for ${id}`);
                await cache.put(id, clonedResponse);
                return networkResponse;

            } catch {
                const cachedResponse = await cache.match(id);

                if (cachedResponse) {
                    ASSET_CACHE_NAME_LOG(`Using cache for ${id}`);
                    return cachedResponse;
                }

                return networkResponse;
            }
        })()
    );

})
// useQueryAsset.ts
import {useQuery} from "@tanstack/react-query";
import {EvamApi} from "@evam-life/sdk";

/**
 * Query hook to fetch an asset from CS
 * @param src
 */
const useQueryAsset = (src: string) => useQuery({
    queryFn: async () => {
        const {data} = await EvamApi.cs.instance.get(src, {
            responseType: "blob"
        });
      
        return URL.createObjectURL(data)
    },
    queryKey: [src],
    refetchOnMount: "always"
})
export default useQueryAsset

(I am using Tanstack Router for routing)

// pdf.tsx

import {createFileRoute, Link, redirect} from '@tanstack/react-router'
import {z} from "zod";
import {Box, Button, Center, Heading, HStack, IconButton, VStack} from "@chakra-ui/react";
import {useRef, useState} from "react";
import {Document, Page as PDFPage} from "react-pdf";
import {ArrowBackRounded} from "@mui/icons-material";
import * as _ from "lodash";
import {useQueryAsset} from "@/hooks";

const pdfPageSearchParser = z.object({
    src: z.string().endsWith(".pdf"),
    infoCardId: z.string()
})
export const Route = createFileRoute('/pdf')({
    component: Page,
    validateSearch: pdfPageSearchParser.parse,
    onError: () => {
        throw redirect({to: "/all-info"})
    }
})


function Page() {
    const {src, infoCardId} = Route.useSearch()
    const queryPdf = useQueryAsset(src);
    const [numPages, setNumPages] = useState<number | undefined>(undefined);
    const intervalIdRef = useRef<ReturnType<typeof setInterval> | null>(null);
    const mainBoxRef = useRef<HTMLDivElement | null>(null);
    const headerBoxRef = useRef<HTMLDivElement | null>(null);

    if (queryPdf.isLoading) return null
    if (queryPdf.data === undefined) return <Center h={"100vh"}>
        <VStack alignItems="flex-start" gap={4}>
            <Heading>
                Error displaying PDF
            </Heading>
            <Button
                asChild
                colorPalette={"orange"}
                size={"lg"}>
                <Link
                    search={{
                        source: "all-info"
                    }}
                    params={{infoCardId}}
                    to={"/info-card/$infoCardId"}>
                    Go back
                </Link>
            </Button>
        </VStack>
    </Center>

    const pdfName = _.last(
        src.split("/")
    ) || "PDF";

    if (intervalIdRef.current !== null) clearInterval(intervalIdRef.current)

    return <Box w={"100vw"}
                overflowY={"none"}
                ref={mainBoxRef}
                h={"100vh"}>
        <HStack
            ref={headerBoxRef}
            bg={"black"}
            w={"full"}
            py={"28px"}
            px={"16px"}
            gap={"64px"}
            alignItems={"flex-start"}>
            <IconButton
                color={"white"}
                variant={"plain"}
                asChild>
                <Link
                    search={{
                        source: "all-info"
                    }}
                    params={{infoCardId}}
                    to={"/info-card/$infoCardId"}>
                    <ArrowBackRounded style={{
                        fontSize: "32px"
                    }}/>
                </Link>
            </IconButton>
            <Heading
                fontSize={"32px"}
                color={"white"}>
                {pdfName}
            </Heading>
        </HStack>
        <Box w={"100vw"}
             overflowY={"auto"}
             h={`100vh`}>
            <Document
                file={queryPdf.data}
                onLoadSuccess={({numPages}) => {
                    setNumPages(numPages)
                }}
            >
                {
                    numPages ? Array.from(({
                        length: numPages
                    }))
                        .map((_, index) => <PDFPage
                            width={mainBoxRef.current?.getBoundingClientRect().width}
                            height={mainBoxRef.current?.getBoundingClientRect().height}
                            renderAnnotationLayer={false}
                            renderMode={"canvas"}
                            renderTextLayer={false}
                            pageIndex={index}
                        />) : null
                }
            </Document>
        </Box></Box>
}

Environment

  • Browser (if applicable): Android WebView
  • React-PDF version: latest
  • React version: "react": "^18.3.1"
  • Bundler name and version (if applicable): I don't know
@kainbryanjones kainbryanjones added the bug Something isn't working label Dec 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

1 participant