Closer/functions/dist/notifications/push.js

75 lines
3.6 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getUserTokens = getUserTokens;
exports.batchResponseToResults = batchResponseToResults;
exports.sendPushToUser = sendPushToUser;
const pruneTokens_1 = require("./pruneTokens");
const log_1 = require("../log");
/**
* One canonical FCM-token reader and one canonical sender for the whole codebase.
*
* Before this module, ~10 files re-implemented "read the user's tokens" and ~19 re-implemented
* "send to each token, log failures, prune the dead ones". This collapses both into a single,
* unit-tested unit. Callers keep what is genuinely theirs — gating (quiet hours / per-notif
* toggles) and any in-app `notification_queue` write; this module only touches FCM.
*/
/**
* Merge a user's FCM tokens from the legacy `fcmToken` field and the `fcmTokens` subcollection,
* de-duplicated, order-stable (legacy first). Pass `userData` if the user doc is already loaded to
* avoid a redundant read.
*/
async function getUserTokens(db, uid, userData) {
const tokens = [];
const data = userData !== null && userData !== void 0 ? userData : (await db.collection('users').doc(uid).get()).data();
const legacy = data === null || data === void 0 ? void 0 : data.fcmToken;
if (typeof legacy === 'string' && legacy.length > 0)
tokens.push(legacy);
const snap = await db.collection('users').doc(uid).collection('fcmTokens').get();
snap.docs.forEach((d) => {
var _a;
const t = (_a = d.data()) === null || _a === void 0 ? void 0 : _a.token;
if (typeof t === 'string' && t.length > 0 && !tokens.includes(t))
tokens.push(t);
});
return tokens;
}
/**
* Adapt a multicast `BatchResponse` to the `PromiseSettledResult[]` shape `pruneDeadTokens`
* consumes (index `i` still maps to `tokens[i]`). Exported for unit testing the dead-token path.
*/
function batchResponseToResults(batch) {
return batch.responses.map((r) => r.success
? { status: 'fulfilled', value: r.messageId }
: { status: 'rejected', reason: r.error });
}
/**
* Send one push to every FCM token a user has, in a single batched `sendEachForMulticast`
* round-trip, then prune any tokens FCM reports as permanently dead. Best-effort: never throws;
* returns a per-user summary. A whole-batch failure (auth/quota, not a per-token death) is logged
* and does not prune anything.
*/
async function sendPushToUser(db, messaging, uid, content, userData) {
const tokens = await getUserTokens(db, uid, userData);
if (tokens.length === 0)
return { sent: 0, failed: 0, pruned: 0 };
const message = Object.assign(Object.assign({ tokens, notification: content.notification }, (content.data ? { data: content.data } : {})), (content.android ? { android: content.android } : {}));
let batch;
try {
batch = await messaging.sendEachForMulticast(message);
}
catch (e) {
log_1.logger.warn(`[push] sendEachForMulticast failed for uid=${uid}`, { error: String(e) });
return { sent: 0, failed: tokens.length, pruned: 0 };
}
batch.responses.forEach((r, i) => {
var _a;
if (!r.success) {
log_1.logger.warn(`[push] FCM send failed uid=${uid} token=${(0, log_1.redactToken)(tokens[i])}`, {
code: (_a = r.error) === null || _a === void 0 ? void 0 : _a.code,
});
}
});
const pruned = await (0, pruneTokens_1.pruneDeadTokens)(db, uid, tokens, batchResponseToResults(batch));
return { sent: batch.successCount, failed: batch.failureCount, pruned };
}
//# sourceMappingURL=push.js.map