Service Worker Project — Part 3: Two-Way Messaging 📨

March 5, 20263 min read
service workerPWAofflinecachingweb workersprogressive web appsbackground sync

Lecture 10: Message Handling in the Service Worker 📬

New Variables at the Top of sw.js

Bump the version and add two state variables:

const version = 2; let isOnline = true; // assume online until told otherwise let isLoggedIn = false; // assume logged out until told otherwise

These are the two pieces of state the SW can't discover itself — the page will keep them updated via messages.


Requesting a Status Update on Start

Inside main(), as soon as the SW starts, immediately ask all connected pages for their current status:

async function main() { console.log(`Service Worker (v${version}) is starting`); await sendMessage({ requestStatusUpdate: true }); }

This handles the restart scenario — if the SW was killed and revived, it has no idea what the current online/login state is. Asking the page is the simplest solution.


sendMessage() — Broadcasting to All Clients

This is the more complex side of messaging. Because the SW controls multiple pages, it needs to send to all of them:

async function sendMessage(msg) { let allClients = await clients.matchAll({ includeUncontrolled: true }); return Promise.all( allClients.map(sendClientMessage) ); async function sendClientMessage(client) { let chan = new MessageChannel(); chan.port1.onmessage = onMessage; // listen for replies on port1 return client.postMessage(msg, [chan.port2]); // send port2 to the client } }

Three key concepts here:

  1. clients.matchAll() — gets all connected pages, including uncontrolled ones (pages that loaded before this SW was active)
  2. MessageChannel — creates a dedicated two-port communication line for each client. port1 is the SW's end, port2 is the client's end
  3. Promise.all() — waits for all clients to receive the message before resolving

Port1 = SW listens here. Port2 = page listens here. The SW sends port2 to the client so the client knows where to reply.


onMessage() — Receiving Status Updates

Add a listener for incoming messages and handle the statusUpdate response:

self.addEventListener('message', onMessage); function onMessage({ data }) { if (data.statusUpdate) { ({ isOnline, isLoggedIn } = data.statusUpdate); console.log(`Service Worker (v${version}) status update — isOnline: ${isOnline}, isLoggedIn: ${isLoggedIn}`); } }

Destructuring assignment pulls isOnline and isLoggedIn directly out of data.statusUpdate and updates the SW's local variables in one line.


Testing It Works ✅

After refreshing, the console shows:

Service Worker (v2) installed Service Worker (v2) status: isOnline: true, isLoggedIn: false

Toggle offline in DevTools → SW immediately logs the new state. Log in → SW logs isLoggedIn: true. Log out → SW logs isLoggedIn: false.

The two-way messaging pipeline is fully working.


Watch Out: Multiple Clients, Single State ⚠️

Kyle raised an important caveat worth keeping in mind:

The SW stores a single isOnline and isLoggedIn variable — but it's potentially receiving updates from multiple tabs. If the user has three tabs open, all three could be sending status updates, and the last one wins.

For this simple blog this is fine — it's hard to imagine being online in one tab and offline in another. But for a more complex app with meaningful per-tab state, you'd want to track state per client rather than globally.

"If you're keeping stuff in sync across multiple clients, be careful of race conditions. You might be getting different information from different tabs."


The Complete sw.js So Far

const version = 2; let isOnline = true; let isLoggedIn = false; self.addEventListener('install', onInstall); self.addEventListener('activate', onActivate); self.addEventListener('message', onMessage); async function main() { console.log(`Service Worker (v${version}) is starting`); await sendMessage({ requestStatusUpdate: true }); } main().catch(console.error); async function onInstall(event) { event.waitUntil(handleInstall()); self.skipWaiting(); } async function onActivate(event) { event.waitUntil(handleActivation()); } async function handleInstall() { console.log(`Service Worker (v${version}) installed`); } async function handleActivation() { await clients.claim(); console.log(`Service Worker (v${version}) activated`); } async function sendMessage(msg) { let allClients = await clients.matchAll({ includeUncontrolled: true }); return Promise.all(allClients.map(sendClientMessage)); async function sendClientMessage(client) { let chan = new MessageChannel(); chan.port1.onmessage = onMessage; return client.postMessage(msg, [chan.port2]); } } function onMessage({ data }) { if (data.statusUpdate) { ({ isOnline, isLoggedIn } = data.statusUpdate); console.log(`Service Worker (v${version}) — isOnline: ${isOnline}, isLoggedIn: ${isLoggedIn}`); } }