Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | 3x 3x 3x 19x 19x 18x 14x 14x 20x 20x 20x 18x 18x 15x 14x 10x 15x 10x 10x 1x 3x | /**
* Roadmap update notification trigger.
*
* Queries all subscribers who opted into roadmapUpdate notifications,
* then dispatches notifications INLINE per subscriber via
* `dispatchNotificationInline`. No persistent queue, no cron.
*
* Fire-and-forget at the route layer (callers in routes/suggestions.js
* already use `.catch(...)` and don't await this function), so the
* admin HTTP response returns immediately while dispatch fans out.
*/
const { db } = require('./firebase');
const { dispatchNotificationInline } = require('./notification-channels');
const log = require('./log');
/**
* Notify all roadmapUpdate subscribers about a roadmap change.
*
* @param {string} message - Description of what changed (shown to users)
*/
async function notifyRoadmapSubscribers(message) {
try {
// Server-side filter on the denormalised `roadmapUpdateOptedIn` flag.
// A full-collection scan would cost a read per subscriber every
// roadmap edit; the flag is maintained on every PUT /subscriptions/me.
const snap = await db
.collection('subscriptions')
.where('roadmapUpdateOptedIn', '==', true)
.get();
if (snap.empty) return;
// Dispatch in parallel per subscriber. Each dispatch is wrapped in
// its own try/catch inside `dispatchNotificationInline`, so a
// per-subscriber failure is contained — Promise.allSettled prevents
// one rejected promise from cancelling the whole fan-out.
const dispatches = [];
for (const doc of snap.docs) {
const sub = doc.data();
const prefs = sub.channelPreferences?.roadmapUpdate;
// Defensive double-check: if the denormalised flag drifted from
// the actual prefs (race between PUT and notify), trust the prefs.
if (!prefs) continue;
const hasAnyChannel = prefs.email || prefs.push || prefs.inApp || prefs.systemMessage;
if (!hasAnyChannel) continue;
dispatches.push(
dispatchNotificationInline({
type: 'roadmapUpdate',
uid: sub.uid || doc.id,
title: 'Roadmap Update',
body: message,
channels: {
email: !!prefs.email,
push: !!prefs.push,
inApp: !!prefs.inApp,
systemMessage: !!prefs.systemMessage,
},
email: sub.email || null,
pushToken: sub.pushToken || null,
}),
);
}
if (dispatches.length === 0) return;
const settled = await Promise.allSettled(dispatches);
const sent = settled.filter((r) => r.status === 'fulfilled').length;
const failed = settled.length - sent;
log.info('roadmap-notify', `Dispatched ${sent} roadmap notifications inline (${failed} threw)`);
} catch (err) {
log.error('roadmap-notify', 'Failed to notify roadmap subscribers', {
error: err.message,
});
}
}
module.exports = { notifyRoadmapSubscribers };
|