v0.12.14 FCM optimization

This commit is contained in:
2026-03-24 08:22:56 -04:00
parent bb5a3b6813
commit 117b5cbe4c
7 changed files with 251 additions and 44 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "rosterchirp-frontend",
"version": "0.12.13",
"version": "0.12.14",
"private": true,
"scripts": {
"dev": "vite",

View File

@@ -0,0 +1,59 @@
// ── Unified Push Handler (Optimized for Mobile) ──────────────────────────────
self.addEventListener('push', (event) => {
console.log('[SW] Push event received. Messaging Ready:', !!messaging);
// event.waitUntil is the "Keep-Alive" signal for mobile OS
event.waitUntil(
(async () => {
try {
let payload;
// 1. Try to parse the data directly from the push event (Fastest/Reliable)
if (event.data) {
try {
payload = event.data.json();
console.log('[SW] Raw push data parsed:', JSON.stringify(payload));
} catch (e) {
console.warn('[SW] Could not parse JSON, using text fallback');
payload = { notification: { body: event.data.text() } };
}
}
// 2. If the payload is empty, check if Firebase can catch it
// (This happens if your server sends "Notification" instead of "Data" messages)
if (!payload && messaging) {
// This is a last-resort wait for the SDK
payload = await new Promise((resolve) => {
const timeout = setTimeout(() => resolve(null), 2000);
messaging.onBackgroundMessage((bgPayload) => {
clearTimeout(timeout);
resolve(bgPayload);
});
});
}
// 3. Construct and show the notification
if (payload) {
const n = payload.notification || {};
const d = payload.data || {};
// Use the specific function you already defined
await showRosterChirpNotification({
title: n.title || d.title || 'New Message',
body: n.body || d.body || '',
url: d.url || d.link || '/', // some SDKs use 'link'
groupId: d.groupId || '',
});
} else {
// Fallback if we woke up for a "ghost" push with no data
await self.registration.showNotification('RosterChirp', {
body: 'You have a new update.',
tag: 'rosterchirp-fallback'
});
}
} catch (error) {
console.error('[SW] Critical Push Error:', error);
}
})()
);
});

View File

@@ -0,0 +1,82 @@
The Consolidated "Bulletproof" Push Listener
To fix the "hit or miss" behavior on mobile, we need to move away from relying on the Firebase SDK's internal listener (which is a black box that doesn't always play nice with mobile power management) and instead wrap everything in the native push event using event.waitUntil.
Replace your current messaging.onBackgroundMessage and self.addEventListener('push') blocks with this unified version:
JavaScript
// ── Unified Push Handler (Optimized for Mobile) ──────────────────────────────
self.addEventListener('push', (event) => {
console.log('[SW] Push event received. Messaging Ready:', !!messaging);
// event.waitUntil is the "Keep-Alive" signal for mobile OS
event.waitUntil(
(async () => {
try {
let payload;
// 1. Try to parse the data directly from the push event (Fastest/Reliable)
if (event.data) {
try {
payload = event.data.json();
console.log('[SW] Raw push data parsed:', JSON.stringify(payload));
} catch (e) {
console.warn('[SW] Could not parse JSON, using text fallback');
payload = { notification: { body: event.data.text() } };
}
}
// 2. If the payload is empty, check if Firebase can catch it
// (This happens if your server sends "Notification" instead of "Data" messages)
if (!payload && messaging) {
// This is a last-resort wait for the SDK
payload = await new Promise((resolve) => {
const timeout = setTimeout(() => resolve(null), 2000);
messaging.onBackgroundMessage((bgPayload) => {
clearTimeout(timeout);
resolve(bgPayload);
});
});
}
// 3. Construct and show the notification
if (payload) {
const n = payload.notification || {};
const d = payload.data || {};
// Use the specific function you already defined
await showRosterChirpNotification({
title: n.title || d.title || 'New Message',
body: n.body || d.body || '',
url: d.url || d.link || '/', // some SDKs use 'link'
groupId: d.groupId || '',
});
} else {
// Fallback if we woke up for a "ghost" push with no data
await self.registration.showNotification('RosterChirp', {
body: 'You have a new update.',
tag: 'rosterchirp-fallback'
});
}
} catch (error) {
console.error('[SW] Critical Push Error:', error);
}
})()
);
});
Why this fixes your issues:
Eliminates the Race Condition: By wrapping the logic in an async function inside event.waitUntil, you tell Android/iOS: "Do not kill this process until the notification is actually rendered on the screen."
Bypasses SDK "Sleep": Sometimes the Firebase SDK takes too long to initialize. This code tries to parse the event.data directly first. Direct parsing is almost instantaneous, which is critical when a phone is in a deep sleep state.
Handles "Ghost" Pushes: Sometimes FCM sends a "heartbeat" or an empty push to wake the device. This script ensures that even if data is missing, the worker stays alive long enough to handle it gracefully rather than crashing.
One Final Server-Side Check
Since you are using HAProxy, check your server logs for the HTTP status code when you send a message to a mobile device.
If you see 200 OK but no notification: The issue was definitely this Service Worker "sleep" issue.
If you see 401 or 403: HAProxy might be stripping the Authorization header from your backend's outbound request to Google.
If you see 400: Ensure your backend is sending priority: "high" in the FCM JSON.
Would you like me to provide a Python or Node.js snippet to test sending a "High Priority" message with the correct v1 API headers?

View File

@@ -75,58 +75,47 @@ function showRosterChirpNotification(data) {
});
}
// ── FCM background messages ───────────────────────────────────────────────────
// Server sends notification+data messages (mirrors the working fcm-app pattern).
// payload.notification carries title/body; payload.data carries url/groupId.
// Fallback to payload.data.title/body supports any older data-only messages still
// in-flight during a deployment transition.
if (messaging) {
messaging.onBackgroundMessage((payload) => {
console.log('[SW] onBackgroundMessage received:', JSON.stringify({ notification: payload.notification, data: payload.data }));
const n = payload.notification || {};
const d = payload.data || {};
return showRosterChirpNotification({
title: n.title || d.title || 'New Message',
body: n.body || d.body || '',
url: d.url || '/',
groupId: d.groupId || '',
});
});
} else {
console.warn('[SW] Firebase messaging not initialised — push notifications disabled');
}
// ── Raw push event (fallback for Android) ─────────────────────────────────────
// Only runs when Firebase messaging is NOT yet initialised (e.g. push arrives
// before firebaseConfigPromise resolves). If Firebase IS ready it already has
// its own push listener registered and will call onBackgroundMessage — running
// this handler too would show the notification twice.
// ── Push handler ──────────────────────────────────────────────────────────────
// Unified handler — always uses event.waitUntil so the mobile OS does not
// terminate the SW before the notification is shown. Parses event.data
// directly (fast, reliable) rather than delegating to the Firebase SDK's
// internal push listener, which can be killed before it finishes on Android.
self.addEventListener('push', (event) => {
console.log('[SW] push event received, hasData:', !!event.data, 'messaging ready:', !!messaging);
// Firebase compat SDK handles it via onBackgroundMessage — skip to avoid double notification.
if (messaging) return;
if (!event.data) return;
console.log('[SW] Push received, hasData:', !!event.data, 'messaging:', !!messaging);
event.waitUntil((async () => {
try {
const data = event.data.json();
console.log('[SW] Push fallback — data parsed:', JSON.stringify(data));
if (data.data || (data.title && data.body)) {
await showRosterChirpNotification(data.data || data);
let payload = null;
if (event.data) {
try {
payload = event.data.json();
console.log('[SW] Push data:', JSON.stringify({ notification: payload.notification, data: payload.data }));
} catch (e) {
console.warn('[SW] Push data not JSON:', e);
}
}
} catch (e) {
console.warn('[SW] Push fallback — failed to parse push data:', e);
const text = event.data.text();
if (text) {
if (payload) {
const n = payload.notification || {};
const d = payload.data || {};
await showRosterChirpNotification({
title: n.title || d.title || 'New Message',
body: n.body || d.body || '',
url: d.url || '/',
groupId: d.groupId || '',
});
} else {
// Ghost push — keep SW alive and show a generic notification
await self.registration.showNotification('RosterChirp', {
body: text.slice(0, 100),
body: 'You have a new message.',
icon: '/icons/icon-192.png',
badge: '/icons/icon-192-maskable.png',
tag: 'rosterchirp-fallback',
});
}
} catch (e) {
console.error('[SW] Push handler error:', e);
}
})());
});