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 82 83 84 85 86 87 88 | 1x 1x 1x 1x 6x 6x 1x 6x 1x 1x 5x 5x 5x 5x 5x 908x 908x 504x 504x 404x 404x 404x 404x 404x 1x 1x 1x 5x 2x 5x 2x 3x 1x | /**
* One-shot backfill cron: populate `roadmapUpdateOptedIn` on every legacy
* subscription doc that doesn't yet have the field.
*
* Phase 2A finding #2 introduced a denormalised flag on the `subscriptions`
* collection so `roadmap-notify.js` can run a server-side equality filter
* instead of scanning the whole collection. New subscriptions written after
* the route fix automatically include the field; the existing subscriber
* base needs a one-time backfill.
*
* Strategy: paginate through subscriptions in CRON_LIMIT-sized pages, and
* for each doc that lacks `roadmapUpdateOptedIn`, compute the value from
* `channelPreferences.roadmapUpdate` and write it. Self-stops once a tick
* processes 0 missing-field docs.
*
* Reads/writes scale with the number of legacy subscribers. At 5K subs
* with a 500/tick cap that's ~10 ticks (10 days at the daily cadence) to
* fully migrate; the cron is idempotent so manual triggers via the admin
* panel are safe.
*/
const { db } = require('../utils/firebase');
const log = require('../utils/log');
const { computeRoadmapOptedIn } = require('../utils/notification-prefs');
// Pattern matches expireBans/expireDataExports/rotateLogs Phase 2 fixes.
const CRON_LIMIT = 500;
async function backfillRoadmapOptedIn() {
// Read up to CRON_LIMIT subscription docs. We can't `.where('field', '==', undefined)`
// on Firestore directly, so we fetch a page and filter client-side. Once
// every legacy doc has the field set, this scan returns docs that already
// have it (no-op writes are skipped) and the cron quietly finishes.
const snap = await db.collection('subscriptions').limit(CRON_LIMIT).get();
if (snap.size === CRON_LIMIT) {
log.warn('cron', 'backfillRoadmapOptedIn: hit CRON_LIMIT — backfill still in progress', {
limit: CRON_LIMIT,
});
}
if (snap.empty) {
log.info('cron', 'backfillRoadmapOptedIn: no subscriptions to backfill');
return;
}
let backfilled = 0;
let alreadySet = 0;
let batch = db.batch();
let batchOps = 0;
for (const doc of snap.docs) {
const data = doc.data();
if (typeof data.roadmapUpdateOptedIn === 'boolean') {
alreadySet++;
continue;
}
const flag = computeRoadmapOptedIn(data.channelPreferences?.roadmapUpdate);
batch.update(doc.ref, { roadmapUpdateOptedIn: flag });
backfilled++;
batchOps++;
// Firestore batch limit is 500; flush + restart at 400 to leave headroom.
if (batchOps >= 400) {
await batch.commit();
batch = db.batch();
batchOps = 0;
}
}
if (batchOps > 0) {
await batch.commit();
}
if (backfilled > 0) {
log.info('cron', 'backfillRoadmapOptedIn: backfill progress', {
backfilled,
alreadySet,
pageSize: snap.size,
});
} else {
log.info('cron', 'backfillRoadmapOptedIn: complete (no docs missing the field)', {
alreadySet,
});
}
}
module.exports = backfillRoadmapOptedIn;
|