feat(push): smart push queueing with page-visibility fast path and app-ping/pong fallback
This commit is contained in:
+27
-12
@@ -5,6 +5,7 @@ import { pushSubs as dbPushSubs, type PushSubRow } from './db.js';
|
||||
|
||||
interface PushSubscriptionEntry {
|
||||
endpoint: string;
|
||||
deviceId?: string;
|
||||
subscription: {
|
||||
endpoint: string;
|
||||
keys: { p256dh: string; auth: string };
|
||||
@@ -33,7 +34,7 @@ export function initPush(config: AppConfig): void {
|
||||
console.log('[push] Generated new VAPID keys');
|
||||
}
|
||||
|
||||
const email = process.env.VAPID_EMAIL || 'noreply@clawtap.local';
|
||||
const email = process.env.VAPID_EMAIL || 'izackp@gmail.com';
|
||||
cachedVapidPublicKey = vapidKeys.publicKey;
|
||||
webpush.setVapidDetails(`mailto:${email}`, vapidKeys.publicKey, vapidKeys.privateKey);
|
||||
|
||||
@@ -41,6 +42,7 @@ export function initPush(config: AppConfig): void {
|
||||
const rows = dbPushSubs.getAll();
|
||||
subscriptions = rows.map(row => ({
|
||||
endpoint: row.endpoint,
|
||||
deviceId: row.device_id ?? undefined,
|
||||
subscription: {
|
||||
endpoint: row.endpoint,
|
||||
keys: { p256dh: row.p256dh, auth: row.auth },
|
||||
@@ -54,12 +56,16 @@ export function getVapidPublicKey(): string | null {
|
||||
return cachedVapidPublicKey;
|
||||
}
|
||||
|
||||
export function saveSubscription(subscription: PushSubscriptionEntry['subscription']): void {
|
||||
// Save to SQLite
|
||||
dbPushSubs.save(subscription.endpoint, subscription.keys.p256dh, subscription.keys.auth);
|
||||
// Update in-memory cache
|
||||
subscriptions = subscriptions.filter(s => s.endpoint !== subscription.endpoint);
|
||||
subscriptions.push({ endpoint: subscription.endpoint, subscription });
|
||||
export function saveSubscription(subscription: PushSubscriptionEntry['subscription'], deviceId?: string): void {
|
||||
// Save to SQLite — removes old endpoints for same device_id
|
||||
dbPushSubs.save(subscription.endpoint, subscription.keys.p256dh, subscription.keys.auth, deviceId ?? null);
|
||||
// Update in-memory cache: remove old entries for same device, add new
|
||||
if (deviceId) {
|
||||
subscriptions = subscriptions.filter(s => s.endpoint === subscription.endpoint || !s.deviceId || s.deviceId !== deviceId);
|
||||
} else {
|
||||
subscriptions = subscriptions.filter(s => s.endpoint !== subscription.endpoint);
|
||||
}
|
||||
subscriptions.push({ endpoint: subscription.endpoint, subscription, deviceId });
|
||||
}
|
||||
|
||||
export function removeSubscription(endpoint: string): void {
|
||||
@@ -89,27 +95,36 @@ export function getPendingSessions(): Record<string, number> {
|
||||
}
|
||||
|
||||
export async function sendPush(payload: unknown): Promise<void> {
|
||||
if (subscriptions.length === 0) return;
|
||||
if (subscriptions.length === 0) {
|
||||
console.log('[push] sendPush: no subscriptions registered, skipping');
|
||||
return;
|
||||
}
|
||||
console.log(`[push] sendPush: sending to ${subscriptions.length} subscription(s)`);
|
||||
|
||||
const body = JSON.stringify(payload);
|
||||
const expired: string[] = [];
|
||||
let errorCount = 0;
|
||||
|
||||
await Promise.allSettled(
|
||||
subscriptions.map(async ({ endpoint, subscription }) => {
|
||||
try {
|
||||
await webpush.sendNotification(subscription, body);
|
||||
} catch (err) {
|
||||
const e = err as { statusCode?: number; message?: string };
|
||||
if (e.statusCode === 410 || e.statusCode === 404) {
|
||||
// Subscription expired — mark for removal
|
||||
const e = err as { statusCode?: number; message?: string; body?: unknown };
|
||||
const bodyReason = (() => { try { return JSON.parse(e.body as string)?.reason; } catch { return null; } })();
|
||||
if (e.statusCode === 410 || e.statusCode === 404 || bodyReason === 'BadJwtToken') {
|
||||
expired.push(endpoint);
|
||||
} else {
|
||||
console.error(`[push] Failed to send to ${endpoint.slice(0, 50)}:`, e.message);
|
||||
errorCount++;
|
||||
console.error(`[push] Failed to send to ${endpoint.slice(0, 50)}: status=${e.statusCode} msg=${e.message} body=${JSON.stringify(e.body)}`);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const ok = subscriptions.length - expired.length - errorCount;
|
||||
console.log(`[push] sendPush: done (${ok} ok, ${expired.length} expired, ${errorCount} failed)`);
|
||||
|
||||
// Clean up expired subscriptions
|
||||
if (expired.length > 0) {
|
||||
for (const ep of expired) {
|
||||
|
||||
Reference in New Issue
Block a user