vue.jsprogressive-web-appsparceljs

Why does my service worker in my PWA work fine when online but break when offline (built with Parcel.js)?


The question

My service worker installs, and fetches files from the Cache Storage, perfectly when online.

But as soon as I go offline it can't find anything. What am I doing wrong please?

Perhaps I'm loading things in the wrong order. Or using async incorrectly in my main js file. Or Parcel's list of files to cache in the manifest is insufficient.

The code

Here's my index.html file:

<!DOCTYPE html>

<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Journal</title>      
        <meta name="HandheldFriendly" content="True">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <meta name="theme-color" media="(prefers-color-scheme: light)" content="hsl(45, 100%, 50%)">
        <meta name="theme-color" media="(prefers-color-scheme: dark)"  content="hsl(45, 100%, 50%)">
        <link rel="shortcut icon" href="media/img/logo.svg">        
        <link rel="stylesheet" href="css/main.css">
        <link rel="manifest" href="./app.webmanifest">
    </head>

    <body onload="document.body.style.visibility=`visible`;">
        <!-- avoid flash of unstyled content -->
        <script>document.body.style.visibility=`hidden`;</script>
        <div class="app">
        </div>
        <script src="main.ts" type="module"></script>
    </body>
</html>

My main.ts file

import { createApp } from 'vue';
import {router} from './js/4_widgets/router';
import appWidget from './js/4_widgets/app.vue';
import * as storeHelper from './js/3_io/storeHelper';
import * as globalState from './js/2_logic/state';
import * as testData from './js/1_data/testData';
import * as browserStore from './js/3_io/browserStore';

setupApp();

async function setupApp()
{
    globalThis.__VUE_OPTIONS_API__ = (process.env.NODE_ENV === "development") as unknown as string; //todo make this prod eventually
    globalThis.__VUE_PROD_DEVTOOLS__ = (process.env.NODE_ENV === "development") as unknown as string;

    await registerServiceWorker();
    await storeHelper.start();

    createApp(appWidget)
        .use(router)
        .mount(`.app`);
}

async function registerServiceWorker(): Promise<void>
{
    if ('serviceWorker' in navigator)
    {
        try
        {
            const registration = await navigator.serviceWorker.register(new URL('./serviceWorker.ts', import.meta.url), {type: 'module', scope: '/' });
            console.log('Service worker registered', registration);
        }
        catch (error)
        {
            console.error('Error registering service worker:', error);
        }
    }
    else console.warn('Service workers are not supported.');
}

and my serviceWorker.ts file:

/// <reference types="serviceworker" />

import {manifest, version} from '@parcel/service-worker';

self.addEventListener('offline', (event: Event) =>
{
    console.log('Offline!');
});

self.addEventListener('install', async (event: ExtendableEvent) =>
{
    try
    {
        const cache = await caches.open(version);
        // console.dir(manifest);
        await cache.addAll(manifest); // cache the application's resources.
        await self.skipWaiting(); //makes this serviceworker active immediately
        console.log('Service worker installed');
    }
    catch (error)
    {
        console.error('Service worker installation failed:', error);
    }
});

self.addEventListener('activate', async (event: Event) =>
{
    const keys = await caches.keys();
    await Promise.all(
        keys.map(key => key !== version && caches.delete(key)) //delete old caches if new version found
    );
    console.log('Service worker activated');
});

self.addEventListener('fetch', async (event: FetchEvent) =>
{
    try
    {
        console.log(`${event.request.method} - ${event.request.url} - request received and we are online: ${navigator.onLine}`);
        if (event.request.method !== "GET")  // Always fetch non-GET requests from the network
            return fetch(event.request);
        const response: Response | undefined = await caches.match(event.request); // If the request is for an asset that is cached, serve it from the cache.
        if (!response)
            console.warn(`${event.request.method} - ${event.request.url} - was NOT found in the cache`);
        console.log(`${event.request.method} - ${event.request.url} - was found in the cache`);
        if (response)
            return response;
        if (navigator.onLine)
        {
            console.log(`${event.request.method} - ${event.request.url} - we are online, so trying to connect...`);
            return fetch(event.request);
        }
        console.error(`${event.request.method} - ${event.request.url} - is not in cache and we are offline`);
    }
    catch (error)
    {
        console.error('Service worker fetch failed:', error);
    }
});

The debug trail

Here's what happens in the console when I browse to localhost:1234 in Brave:

First page load, installation works:

Service worker registered ServiceWorkerRegistration {installing: ServiceWorker, waiting: null, active: null, navigationPreload: NavigationPreloadManager, scope: 'http://localhost:1234/', …}
serviceWorker.ts:32 Service worker activated
serviceWorker.ts:18 Service worker installed

Page refresh, cache items fetched (but note it doesn't try to fetch index.html from the cache, which I thought it should):

GET - http://localhost:1234/index.34df27b5.css - was found in the cache
serviceWorker.js?1684145072288:641 GET - http://localhost:1234/index.b7a05eb9.js - request received and we are online: true
serviceWorker.js?1684145072288:645 GET - http://localhost:1234/index.b7a05eb9.js - was found in the cache
serviceWorker.js?1684145072288:641 GET - http://localhost:1234/logo.92616178.svg - request received and we are online: true
serviceWorker.js?1684145072288:641 GET - http://localhost:1234/app.webmanifest - request received and we are online: true
serviceWorker.js?1684145072288:645 GET - http://localhost:1234/logo.92616178.svg - was found in the cache
serviceWorker.js?1684145072288:645 GET - http://localhost:1234/app.webmanifest - was found in the cache
main.ts:39 Service worker registered ServiceWorkerRegistration {installing: ServiceWorker, waiting: null, active: ServiceWorker, navigationPreload: NavigationPreloadManager, scope: 'http://localhost:1234/', …}
:1234/serviceWorker.js?1684145072288:641 GET - http://localhost:1234/appIcon512.7b148678.png - request received and we are online: true
:1234/serviceWorker.js?1684145072288:645 GET - http://localhost:1234/appIcon512.7b148678.png - was found in the cache
serviceWorker.ts:32 Service worker activated
serviceWorker.ts:18 Service worker installed

Go offline and refresh (noticed here the Cache Storage is now empty, but apparently that is actually not true: Cache Storage created by service worker disappears after going offline and refreshing page), cache items not found. And the navigator.online property isn't set to false:

bug/1173575, non-JS module files deprecated.
(anonymous) @ VM11:161
serviceWorker.ts:39 GET - http://localhost:1234/ - request received and we are online: true
serviceWorker.ts:44 GET - http://localhost:1234/ - was NOT found in the cache
(anonymous) @ serviceWorker.ts:44
serviceWorker.ts:45 GET - http://localhost:1234/ - was found in the cache
serviceWorker.ts:50 GET - http://localhost:1234/ - we are online, so trying to connect...
serviceWorker.ts:51     Uncaught (in promise) TypeError: Failed to fetch
    at serviceWorker.ts:51:20
(anonymous) @ serviceWorker.ts:51
serviceWorker.ts:39 GET - http://localhost:1234/ - request received and we are online: true
serviceWorker.ts:44 GET - http://localhost:1234/ - was NOT found in the cache
(anonymous) @ serviceWorker.ts:44
serviceWorker.ts:45 GET - http://localhost:1234/ - was found in the cache
serviceWorker.ts:50 GET - http://localhost:1234/ - we are online, so trying to connect...
serviceWorker.ts:51     Uncaught (in promise) TypeError: Failed to fetch
    at serviceWorker.ts:51:20
(anonymous) @ serviceWorker.ts:51
serviceWorker.ts:39 GET - http://localhost:1234/ - request received and we are online: true
serviceWorker.ts:44 GET - http://localhost:1234/ - was NOT found in the cache
(anonymous) @ serviceWorker.ts:44
serviceWorker.ts:45 GET - http://localhost:1234/ - was found in the cache
serviceWorker.ts:50 GET - http://localhost:1234/ - we are online, so trying to connect...
serviceWorker.ts:51     Uncaught (in promise) TypeError: Failed to fetch
    at serviceWorker.ts:51:20

(If I set my browser to http://localhost:1234/index.html instead, the service worker says the file is found, but no other code fires and the page is not displayed on the screen).

Refresh again, everything's dead:

bug/1173575, non-JS module files deprecated.

Solution

  • Your fetch event handler isn't actually calling event.respondWith(). Just returning the Response from your event handler does nothing.