Skip to content

Service Worker Documentation

Overview

This document provides an overview and detailed explanation of the service worker implementation. The service worker is responsible for handling caching strategies, managing sync events, and ensuring offline capabilities for the application. It heavily utilizes the Workbox library to simplify and enhance the service worker's functionality.

Detailed Sections

Import Statements and References

The service worker script starts with several import statements and references to necessary libraries and types.

/// <reference lib="WebWorker" />
/// <reference types="vite/client" />
/// <reference no-default-lib="true"/>
/// <reference lib="esnext" />
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
import { ExpirationPlugin } from 'workbox-expiration/ExpirationPlugin';
import { cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { DataPusher } from './lib/offline/DataPusher';

Service Worker Installation

The install event listener forces the waiting service worker to become the active one immediately.

self.addEventListener('install', (event) => {
    self.skipWaiting();
});

Message Event Listener

The message event listener listens for messages from the client and skips waiting if the message type is SKIP_WAITING.

self.addEventListener('message', (event) => {
    if (event.data && event.data.type === 'SKIP_WAITING') self.skipWaiting();
});

Activation Event Listener

The activate event listener claims control of the clients immediately after activation.

self.addEventListener('activate', (event) => {
    console.log('Claimed control after activate');
    event.waitUntil(self.clients.claim());
});

Precaching and Cleanup

The service worker uses precacheAndRoute to precache assets and cleanupOutdatedCaches to clean up old caches.

precacheAndRoute(self.__WB_MANIFEST);
cleanupOutdatedCaches();

API Request Caching

The service worker caches all API GET requests using the NetworkFirst strategy.

const apiRegex = /\/api\//;
registerRoute(
    ({ url, request }) => {
        const apiPathMatch =
            apiRegex.test(url.pathname) &&
            request.method === 'GET' &&
            request.url.startsWith(import.meta.env.VITE_APPLICATION_BASE_URL);
        return apiPathMatch;
    },
    new NetworkFirst({
        cacheName: 'api-cache',
        plugins: [
            new CacheableResponsePlugin({
                statuses: [0, 200]
            }),
            new ExpirationPlugin({
                maxAgeSeconds: 14 * 24 * 60 * 60
            })
        ]
    })
);

Document Path Caching

The service worker caches all school document paths using the NetworkFirst strategy.

registerRoute(
    ({ url, request }) => {
        const documentPathMatch = request.url.startsWith(import.meta.env.VITE_SITE_URL);
        return documentPathMatch;
    },
    new NetworkFirst({
        cacheName: 'page-cache',
        plugins: [
            new CacheableResponsePlugin({
                statuses: [0, 200]
            }),
            new ExpirationPlugin({
                maxAgeSeconds: 14 * 24 * 60 * 60
            })
        ]
    })
);

Image Caching

The service worker caches all images using the StaleWhileRevalidate strategy.

registerRoute(
    ({ url, request }) => {
        const imagePath = request.url.startsWith(import.meta.env.VITE_ASSETS_PREFIX);
        return imagePath;
    },
    new StaleWhileRevalidate({
        cacheName: 'image-cache',
        plugins: [
            new CacheableResponsePlugin({
                statuses: [0, 200]
            }),
            new ExpirationPlugin({
                maxAgeSeconds: 14 * 24 * 60 * 60
            })
        ]
    })
);

Sync Event Handling

The service worker handles sync events to push attendance data, assessment data, or all data when the device is back online.

self.addEventListener('sync', (event) => {
    console.log('Received sync event', JSON.stringify(event));
    const dataPusher = new DataPusher();
    if (event.tag === 'sync-attendance') {
        event.waitUntil(
            dataPusher.pushAttendanceData().then(() => {
                console.log('processed attendance data');
            })
        );
    } else if (event.tag === 'sync-daily-assessment') {
        event.waitUntil(
            dataPusher.pushAssessmentData().then(() => {
                console.log('processed assessment data');
            })
        );
    } else if (event.tag === 'sync-all-data') {
        event.waitUntil(
            dataPusher.pushAll().then((totalRecordsProcessed) => {
                console.log('processed sync-all-data', totalRecordsProcessed);
                self.clients.matchAll().then((clients) => {
                    clients.forEach((client) => {
                        client.postMessage({
                            command: 'sync-completed',
                            data: {
                                totalRecordsProcessed: totalRecordsProcessed
                            }
                        });
                    });
                });
                if (totalRecordsProcessed > 0) {
                    self.registration.showNotification(
                        `Offline data processed, total records: ${totalRecordsProcessed}`
                    );
                }
            })
        );
    }
});

Periodic Sync Event Handling

The service worker handles periodic sync events to push all data during periodic sync.

self.addEventListener('periodicsync', (event) => {
    console.log('Received periodic sync event', JSON.stringify(event));
    const dataPusher = new DataPusher();
    if (event.tag === 'content-sync') {
        event.waitUntil(
            dataPusher.pushAll().then((totalRecordsProcessed) => {
                console.log('processed sync-all-data during periodic sync', totalRecordsProcessed);
                self.clients.matchAll().then((clients) => {
                    clients.forEach((client) => {
                        client.postMessage({
                            command: 'sync-completed',
                            data: {
                                totalRecordsProcessed: totalRecordsProcessed
                            }
                        });
                    });
                });
                if (totalRecordsProcessed > 0) {
                    self.registration.showNotification(
                        `Offline data processed, total records: ${totalRecordsProcessed}`
                    );
                }
            })
        );
    }
});