import { db, type StoredRow } from "../db/fcmTokens.js"; import { errorMessage, formatElapsedMs } from "../util/formatElapsed.js"; import { maskToken } from "../util/maskToken.js"; import { messaging } from "./firebase.js"; const MS_PRODUCTION = 23 * 60 * 60 * 1000; const MS_TEST = 10 * 60 * 1000; export function notifyThresholdMs(testMode?: boolean): number { return testMode === true ? MS_TEST : MS_PRODUCTION; } /** Epoch ms when the device may receive another push (diagnostics only). */ export function computeNextEligibleAt(row: { lastNotifiedAt?: number; testMode?: boolean; }): number { const threshold = notifyThresholdMs(row.testMode); if (row.lastNotifiedAt === undefined) { return Date.now(); } return row.lastNotifiedAt + threshold; } function lastNotifiedMs(row: StoredRow | undefined): number | undefined { const v = row?.lastNotifiedAt; if (v === undefined) return undefined; if (typeof v === "number") return Number.isNaN(v) ? undefined : v; return undefined; } function stringifyData( payload: Record ): Record { const out: Record = {}; for (const [k, v] of Object.entries(payload)) { out[k] = v === undefined || v === null ? "" : String(v); } return out; } /** * Sends an FCM data message if the token is outside the dedupe window * (23h production, 10m test). */ export async function sendPushToDevice( fcmToken: string, payload: Record = {} ): Promise<"sent" | "skipped" | "failed"> { const suffix = maskToken(fcmToken); const row = await db.getByFcmToken(fcmToken); const now = Date.now(); const last = lastNotifiedMs(row); if ( last !== undefined && now - last < notifyThresholdMs(row?.testMode) ) { return "skipped"; } const sendStarted = Date.now(); console.log("[Push] Send attempt, token suffix:", suffix); try { const data: Record = { ...stringifyData(payload), type: "WAKEUP_PING", }; await messaging.send({ token: fcmToken, apns: { headers: { "apns-push-type": "background", "apns-priority": "5", }, payload: { aps: { contentAvailable: true, }, }, }, data, }); const persisted = await db.getByFcmToken(fcmToken); if (persisted !== undefined) { await db.update(persisted.id, { lastNotifiedAt: Date.now() }); } console.log( "[Push] Send completed in", formatElapsedMs(Date.now() - sendStarted) + ",", "token suffix:", suffix ); return "sent"; } catch (err) { console.error( "[Push] Send failed in", formatElapsedMs(Date.now() - sendStarted) + ",", "token suffix:", suffix + ":", errorMessage(err) ); return "failed"; } }