QwikDev / astro

Qwik + Astro integration
189 stars 13 forks source link

Qwik component stops working after reload #56

Closed alping closed 5 months ago

alping commented 5 months ago

Hi.

I am trying to use Astro with Qwik and deploy to GitLab Pages (static hosting). I have a simple index page with a basic Qwik counter component. Everything works as expected in development mode (npm run dev). However, when I build the site (npm run build) and either deploy it on GitLab Pages or preview it locally (npm run preview) it works initially, but fails after a page reload (CTRL-R) with the following error messages:

Failed to load ‘https://astro-unocss-qwik-starter-alping-se-templates-959b4525659b9f868.gitlab.io/build/q-N3Qf0_UH.js’. A ServiceWorker passed a promise to FetchEvent.respondWith() that rejected with ‘TypeError: null has no properties’.

Loading failed for the module with source “https://astro-unocss-qwik-starter-alping-se-templates-959b4525659b9f868.gitlab.io/build/q-N3Qf0_UH.js”.

Uncaught (in promise) TypeError: error loading dynamically imported module: https://astro-unocss-qwik-starter-alping-se-templates-959b4525659b9f868.gitlab.io/build/q-N3Qf0_UH.js
    d https://astro-unocss-qwik-starter-alping-se-templates-959b4525659b9f868.gitlab.io/:18
    AsyncFunctionThrow self-hosted:856
    (Async: async)
    w https://astro-unocss-qwik-starter-alping-se-templates-959b4525659b9f868.gitlab.io/:18
    v https://astro-unocss-qwik-starter-alping-se-templates-959b4525659b9f868.gitlab.io/:18
    push https://astro-unocss-qwik-starter-alping-se-templates-959b4525659b9f868.gitlab.io/:18
    <anonymous> https://astro-unocss-qwik-starter-alping-se-templates-959b4525659b9f868.gitlab.io/:57

The only way to make it work again is with a hard refresh (CTRL-SHIFT-R). Tested in both Firefox and Edge.

Code available here (minimal): https://gitlab.com/alping-se/templates/astro-unocss-qwik-starter Example website available here: https://astro-unocss-qwik-starter-alping-se-templates-959b4525659b9f868.gitlab.io/ (To see the errors reload the page at least once)

I am not sure if this is a bug or if I have maybe misunderstood how to generate static sites with Astro+Qwik.

- Peter

thejackshelton commented 5 months ago

Even reloading the page I don't see the errors. Hm.... I tried on chrome, firefox, and edge.

Does this happen only on the site preview? Or the deployed site?

alping commented 5 months ago

It happened on both the deployed and preview site. But after completely deleting the cache in the browser(s), I now also don't get the error. Unsure what happened, but it seems to be working as expected now. Probably something on my end. Sorry to have taken your time.

Thank you for your work on this integration, I look forward to exploring it further!

alping commented 5 months ago

Actually, I was a bit too quick in closing this. It seems that the issue arises after the page has been open in the browser for some time, approx. 2-3 min for me.

This happens on the deployed site. I also tried on my phone with the same result, i.e. works initially even after reload, but stops working after reload when it has been open in the browser for a few minutes.

thejackshelton commented 5 months ago

Ah I was able to reproduce after going back to the page.

In Firefox:

image

In Chrome:

image

Not sure if this might be an issue, but it is using the service worker from when I last visited the page 3 hours ago. Clearing the Qwik bundles on the page does not help either.

image

A hard refresh seems to solve it like you mention. @mhevery if you have any further insight. Looks to be on the latest versions.

mhevery commented 5 months ago

should be fixed by: https://github.com/BuilderIO/qwik/pull/5748

thejackshelton commented 5 months ago

Unfortunately not. Tried the latest commit on the qwik build artifacts and here are my findings:

https://github.com/thejackshelton/new-cache-repro

The first issue is this:

1) clone the repo and run pnpm i 2) run build with pnpm build 3) run the preview with pnpm preview 4) click on the counter and then refresh the page. 5) you should see this:

Failed to load resource: net::ERR_FAILED

6(index):5 Uncaught (in promise) TypeError: Failed to fetch dynamically imported module: http://localhost:4321/build/q-BQZEcxxK.js

qwik-prefetch-service-worker.js:1 Uncaught (in promise) TypeError: t.U is not a function
    at qwik-prefetch-service-worker.js:1:2952

https://new-cache-repro.vercel.app/

When I first get into the page I see this in the console:

qwik-prefetch-service-worker.js:1 Uncaught (in promise) TypeError: Cannot read properties of null (reading 'put')
    at qwik-prefetch-service-worker.js:1:1089

And when I refresh the page:

The FetchEvent for "https://new-cache-repro.vercel.app/build/q-BQZEcxxK.js" resulted in a network error response: the promise was rejected.
Promise.then (async)
(anonymous) @ qwik-prefetch-service-worker.js:1
qwik-prefetch-service-worker.js:1 Uncaught (in promise) TypeError: Cannot read properties of null (reading 'match')
    at qwik-prefetch-service-worker.js:1:667
    at qwik-prefetch-service-worker.js:1:787
    at Array.map (<anonymous>)
    at t (qwik-prefetch-service-worker.js:1:408)
    at e (qwik-prefetch-service-worker.js:1:143)
    at qwik-prefetch-service-worker.js:1:2964
(anonymous) @ qwik-prefetch-service-worker.js:1
(anonymous) @ qwik-prefetch-service-worker.js:1
t @ qwik-prefetch-service-worker.js:1
e @ qwik-prefetch-service-worker.js:1
(anonymous) @ qwik-prefetch-service-worker.js:1
(index):5 

       GET https://new-cache-repro.vercel.app/build/q-BQZEcxxK.js net::ERR_FAILED
f @ (index):5
d @ (index):5
4(index):5 Uncaught (in promise) TypeError: Failed to fetch dynamically imported module: https://new-cache-repro.vercel.app/build/q-BQZEcxxK.js

This behavior happens in both the preview environment and deploy environment. I also see that the pr was merged to core @mhevery

dandelp commented 5 months ago

As a temporary workaround until the issue is resolved I am manually copying the following into my build as qwik-prefetch-service-worker.js. Haven't had the issue since.

(() => {
  const DIRECT_PRIORITY = Number.MAX_SAFE_INTEGER >>> 1;
  function directFetch(swState, url) {
    const [basePath, filename] = parseBaseFilename(url);
    const base = swState.$bases$.find((base2) => basePath === base2.$path$);
    if (base) {
      swState.$log$("intercepting", url.pathname);
      return enqueueFileAndDependencies(
        swState,
        base,
        [filename],
        DIRECT_PRIORITY
      ).then(() =>
        (async function (swState, url) {
          const currentRequestTask = swState.$queue$.find(
            (task) => task.$url$.pathname === url.pathname
          );
          if (currentRequestTask) {
            return currentRequestTask.$response$;
          }
          swState.$log$("CACHE HIT", url.pathname);
          return (await swState.$cache$()).match(url);
        })(swState, url)
      );
    }
  }
  async function enqueueFileAndDependencies(
    swState,
    base,
    filenames,
    priority
  ) {
    const fetchSet = new Set();
    filenames.forEach((filename) =>
      addDependencies(base.$graph$, fetchSet, filename)
    );
    await Promise.all(
      Array.from(fetchSet).map((filename) =>
        (async function (swState, url, priority) {
          let task = swState.$queue$.find(
            (task2) => task2.$url$.pathname === url.pathname
          );
          const mode = priority >= DIRECT_PRIORITY ? "direct" : "prefetch";
          if (task) {
            const state = task.$isFetching$ ? "fetching" : "waiting";
            if (task.$priority$ < priority) {
              swState.$log$("queue update priority", state, url.pathname);
              task.$priority$ = priority;
            } else {
              swState.$log$("already in queue", mode, state, url.pathname);
            }
          } else {
            if (!(await (await swState.$cache$()).match(url))) {
              swState.$log$("enqueue", mode, url.pathname);
              task = {
                $priority$: priority,
                $url$: url,
                $resolveResponse$: null,
                $response$: null,
                $isFetching$: !1,
              };
              task.$response$ = new Promise(
                (resolve) => (task.$resolveResponse$ = resolve)
              );
              swState.$queue$.push(task);
            }
          }
          return task;
        })(swState, new URL(base.$path$ + filename, swState.$url$), priority)
      )
    );
    taskTick(swState);
  }
  function taskTick(swState) {
    swState.$queue$.sort(byFetchOrder);
    let outstandingRequests = 0;
    for (const task of swState.$queue$) {
      if (task.$isFetching$) {
        outstandingRequests++;
      } else if (
        outstandingRequests < swState.$maxPrefetchRequests$ ||
        task.$priority$ >= DIRECT_PRIORITY
      ) {
        task.$isFetching$ = !0;
        outstandingRequests++;
        const action =
          task.$priority$ >= DIRECT_PRIORITY ? "FETCH (CACHE MISS)" : "FETCH";
        swState.$log$(action, task.$url$.pathname);
        swState
          .$fetch$(task.$url$)
          .then(async (response) => {
            if (200 === response.status) {
              swState.$log$("CACHED", task.$url$.pathname);
              await (await swState.$cache$()).put(task.$url$, response.clone());
            }
            task.$resolveResponse$(response);
          })
          .finally(() => {
            swState.$log$("FETCH DONE", task.$url$.pathname);
            swState.$queue$.splice(swState.$queue$.indexOf(task), 1);
            taskTick(swState);
          });
      }
    }
  }
  function byFetchOrder(a, b) {
    return b.$priority$ - a.$priority$;
  }
  function addDependencies(graph, fetchSet, filename) {
    if (!fetchSet.has(filename)) {
      fetchSet.add(filename);
      let index = graph.findIndex((file) => file === filename);
      if (-1 !== index) {
        while ("number" == typeof graph[++index]) {
          addDependencies(graph, fetchSet, graph[graph[index]]);
        }
      }
    }
    return fetchSet;
  }
  function parseBaseFilename(url) {
    const pathname = new URL(url).pathname;
    const slashIndex = pathname.lastIndexOf("/");
    return [
      pathname.substring(0, slashIndex + 1),
      pathname.substring(slashIndex + 1),
    ];
  }
  const log = (...args) => {
    // console.log("⚙️ Prefetch SW:", ...args);
  };
  const processMessage = async (state, msg) => {
    const type = msg[0];
    state.$log$("received message:", type, msg[1], msg.slice(2));
    "graph" === type
      ? await processBundleGraph(state, msg[1], msg.slice(2), !0)
      : "graph-url" === type
        ? await (async function (swState, base, graphPath) {
            await processBundleGraph(swState, base, [], !1);
            const response = await directFetch(
              swState,
              new URL(base + graphPath, swState.$url$)
            );
            if (response && 200 === response.status) {
              const graph = await response.json();
              graph.push(graphPath);
              await processBundleGraph(swState, base, graph, !0);
            }
          })(state, msg[1], msg[2])
        : "prefetch" === type
          ? await processPrefetch(state, msg[1], msg.slice(2))
          : "prefetch-all" === type
            ? await (function (swState, basePath) {
                const base = swState.$bases$.find(
                  (base2) => basePath === base2.$path$
                );
                base
                  ? processPrefetch(
                      swState,
                      basePath,
                      base.$graph$.filter((item) => "string" == typeof item)
                    )
                  : console.error(
                      `Base path not found: ${basePath}, ignoring prefetch.`
                    );
              })(state, msg[1])
            : "ping" === type
              ? log("ping")
              : "verbose" === type
                ? (state.$log$ = log)("mode: verbose")
                : console.error("UNKNOWN MESSAGE:", msg);
  };
  async function processBundleGraph(swState, base, graph, cleanup) {
    const existingBaseIndex = swState.$bases$.findIndex(
      (base2) => base2 == base2
    );
    -1 !== existingBaseIndex && swState.$bases$.splice(existingBaseIndex, 1);
    swState.$log$("adding base:", base);
    swState.$bases$.push({
      $path$: base,
      $graph$: graph,
    });
    if (cleanup) {
      const bundles = new Set(graph.filter((item) => "string" == typeof item));
      for (const request of await (await swState.$cache$()).keys()) {
        const [cacheBase, filename] = parseBaseFilename(new URL(request.url));
        const promises = [];
        if (cacheBase === base && !bundles.has(filename)) {
          swState.$log$("deleting", request.url);
          promises.push((await swState.$cache$()).delete(request));
        }
        await Promise.all(promises);
      }
    }
  }
  function processPrefetch(swState, basePath, bundles) {
    const base = swState.$bases$.find((base2) => basePath === base2.$path$);
    base
      ? enqueueFileAndDependencies(swState, base, bundles, 0)
      : console.error(`Base path not found: ${basePath}, ignoring prefetch.`);
  }
  function drainMsgQueue(swState) {
    if (!swState.$msgQueuePromise$ && swState.$msgQueue$.length) {
      const top = swState.$msgQueue$.shift();
      swState.$msgQueuePromise$ = processMessage(swState, top).then(() => {
        swState.$msgQueuePromise$ = null;
        drainMsgQueue(swState);
      });
    }
  }
  ((swScope) => {
    const swState = ((fetch, url, cache) => ({
      $fetch$: fetch,
      $queue$: [],
      $bases$: [],
      $cache$: cache,
      $msgQueue$: [],
      $msgQueuePromise$: null,
      $maxPrefetchRequests$: 10,
      $url$: url,
      $log$: (...args) => {},
    }))(swScope.fetch.bind(swScope), new URL(swScope.location.href), () =>
      swScope.caches.open("QwikBundles")
    );
    swScope.addEventListener("fetch", async (ev) => {
      const request = ev.request;
      if ("GET" === request.method) {
        const response = directFetch(swState, new URL(request.url));
        response && ev.respondWith(response);
      }
    });
    swScope.addEventListener("message", (ev) => {
      swState.$msgQueue$.push(ev.data);
      drainMsgQueue(swState);
    });
    swScope.addEventListener(
      "install",
      () => swScope.skipWaiting()
    );
    swScope.addEventListener("activate", async (event) => {
      //   console.log("activating");
      event.waitUntil(swScope.clients.claim());
    });
  })(globalThis);
})();

The major change is passing the $cache$ as the underlying function as opposed to the result of the function. Similar to how $fetch$ is passed. @mhevery Happy to submit a PR if you have any interest in this approach.

wmertens commented 5 months ago

@dandelp I ended up fixing it in a slightly different way https://github.com/BuilderIO/qwik/commit/c226c20f4222a7bfe714cc9216f5d9114f0652b3

thejackshelton commented 5 months ago

Hey devs!

Highly recommend upgrading to 1.4.2 w/ @builder.io/qwik

and v0.5.1 in the @qwikdev/astro integration

Some service worker caching issues fixed. 👍

alping commented 5 months ago

It seems to be working great now. Thank you all for fixing it so quickly!

thejackshelton commented 5 months ago

It seems to be working great now. Thank you all for fixing it so quickly!

Awesome. And the best part is, it's all instantly interactive! That was the reason this was a problem in the first place, was that we wanted to introduce speculative module fetching in Astro.

https://qwik.builder.io/docs/advanced/speculative-module-fetching/#speculative-module-fetching

You should be able to take that a step further with something like Qwik insights, which automatically optimizes your bundles based on user analytics

https://qwik.builder.io/docs/labs/insights/#-insights