All files / src/cron backfillRoadmapOptedIn.js

100% Statements 33/33
100% Branches 12/12
100% Functions 1/1
100% Lines 33/33

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;