Skip to content

Commit e3e80e6

Browse files
committed
fix: One additional edge case and speed
1 parent bc6c3ea commit e3e80e6

2 files changed

Lines changed: 43 additions & 36 deletions

File tree

tools/cleanupFirebase.ts

Lines changed: 41 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -404,15 +404,25 @@ const fastForwardDiscussions = async (
404404
let updatedCount = 0;
405405
for (const [id, updatedDiscussion] of Object.entries(fastForwardedDiscussions)) {
406406
if (updatedDiscussion) {
407-
verbose(
408-
` Fast-forwarding discussion ${id} from ${discussions[id].currentKey} to ${targetKey}`,
409-
);
410407
const compressed = {
411408
...updatedDiscussion,
412409
selection: updatedDiscussion.selection
413410
? compressSelectionJSON(updatedDiscussion.selection)
414411
: null,
415412
};
413+
// Skip discussions with invalid selections (NaN positions from failed mapping)
414+
if (
415+
compressed.selection &&
416+
(Number.isNaN(compressed.selection.a) || Number.isNaN(compressed.selection.h))
417+
) {
418+
verbose(
419+
` Skipping discussion ${id} - invalid selection after fast-forward (NaN position)`,
420+
);
421+
continue;
422+
}
423+
verbose(
424+
` Fast-forwarding discussion ${id} from ${discussions[id].currentKey} to ${targetKey}`,
425+
);
416426
updates[id] = compressed;
417427
updatedCount++;
418428
}
@@ -580,9 +590,13 @@ const pruneDraft = async (
580590
// Check if this is a corrupted history error
581591
// - "Position X out of range" = missing/corrupted steps
582592
// - "Invalid content for node doc" = reconstructed doc is empty/malformed
593+
// - "Inconsistent open depths" = malformed ProseMirror steps
594+
// - "Cannot read properties of null" = null doc from failed reconstruction
583595
const isCorruptedHistory =
584596
(errorStr.includes('Position') && errorStr.includes('out of range')) ||
585-
errorStr.includes('Invalid content for node');
597+
errorStr.includes('Invalid content for node') ||
598+
errorStr.includes('Inconsistent open depths') ||
599+
errorStr.includes('Cannot read properties of null');
586600

587601
if (isCorruptedHistory && pubId) {
588602
// Try to repair by creating a checkpoint from a Release
@@ -631,39 +645,32 @@ const pruneDraft = async (
631645
const discussionsUpdated = await fastForwardDiscussions(draftRef, pruneThreshold);
632646
localStats.discussionsUpdated += discussionsUpdated;
633647

634-
// Prune changes before the safe threshold
635-
localStats.changesDeleted += await pruneKeysBefore(draftRef, 'changes', pruneThreshold);
636-
637-
// Prune merges before the safe threshold (legacy data)
638-
localStats.mergesDeleted += await pruneKeysBefore(draftRef, 'merges', pruneThreshold);
639-
640-
// Prune checkpoints before the prune threshold (keep threshold checkpoint and any after)
641-
localStats.checkpointsDeleted += await pruneKeysBefore(draftRef, 'checkpoints', pruneThreshold);
648+
// Prune changes, merges, and checkpoints in parallel (they're independent)
649+
const [changesDeleted, mergesDeleted, checkpointsDeleted] = await Promise.all([
650+
pruneKeysBefore(draftRef, 'changes', pruneThreshold),
651+
pruneKeysBefore(draftRef, 'merges', pruneThreshold),
652+
pruneKeysBefore(draftRef, 'checkpoints', pruneThreshold),
653+
]);
654+
localStats.changesDeleted += changesDeleted;
655+
localStats.mergesDeleted += mergesDeleted;
656+
localStats.checkpointsDeleted += checkpointsDeleted;
642657

643658
// Clean up checkpointMap entries for deleted checkpoints
644-
const checkpointMapSnapshot = await draftRef.child('checkpointMap').once('value');
645-
const checkpointMap = checkpointMapSnapshot.val();
646-
if (checkpointMap) {
647-
const oldMapKeys = Object.keys(checkpointMap).filter(
648-
(k) => parseInt(k, 10) < pruneThreshold,
649-
);
650-
if (oldMapKeys.length > 0 && !isDryRun) {
651-
// Use batch update for faster deletion
652-
const updates: Record<string, null> = {};
653-
for (const key of oldMapKeys) {
654-
updates[key] = null;
655-
}
656-
await draftRef.child('checkpointMap').update(updates);
657-
verbose(`${prefix}Cleaned up ${oldMapKeys.length} checkpointMap entries`);
659+
// Reuse checkpointKeys we already fetched earlier
660+
const oldMapKeys = checkpointKeys.filter((k) => k < pruneThreshold).map(String);
661+
if (oldMapKeys.length > 0 && !isDryRun) {
662+
const updates: Record<string, null> = {};
663+
for (const key of oldMapKeys) {
664+
updates[key] = null;
658665
}
666+
await draftRef.child('checkpointMap').update(updates);
667+
verbose(`${prefix}Cleaned up ${oldMapKeys.length} checkpointMap entries`);
659668
}
660669

661-
// Remove deprecated singular checkpoint if we have checkpoints/ entries
662-
if (!isDryRun) {
663-
const hasCheckpoints = (await draftRef.child('checkpoints').once('value')).exists();
664-
if (hasCheckpoints) {
665-
await draftRef.child('checkpoint').remove();
666-
}
670+
// Remove deprecated singular checkpoint if we deleted any checkpoints
671+
// (avoiding extra read - if we pruned checkpoints, the checkpoints/ path exists)
672+
if (!isDryRun && checkpointsDeleted > 0) {
673+
await draftRef.child('checkpoint').remove();
667674
}
668675
};
669676

@@ -784,13 +791,13 @@ const cleanupOrphanedFirebasePaths = async (): Promise<void> => {
784791
}
785792

786793
fetched++;
787-
if (fetched % 500 === 0) {
794+
if (fetched % 1000 === 0) {
788795
log(` Scanned ${fetched}/${legacyPubKeys.length} legacy pubs...`);
789796
}
790797

791798
return { pubKey, childKeys, orphanedBranches, hasValidBranch };
792799
}),
793-
20,
800+
50,
794801
);
795802

796803
// Collect all paths to delete

tools/cron.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ if (process.env.PUBPUB_PRODUCTION === 'true') {
2525
cron.schedule('0 */12 * * *', () => run('Backup DB', 'tools-prod backupDb'), {
2626
timezone: 'UTC',
2727
}); // Every 6 hours
28-
cron.schedule('0 5 * * *', () => run('Email Digest', 'tools-prod emailActivityDigest'), {
28+
cron.schedule('0 13 * * *', () => run('Email Digest', 'tools-prod emailActivityDigest'), {
2929
timezone: 'UTC',
3030
});
3131
cron.schedule(
32-
'0 1 * * 0',
32+
'0 5 * * 0',
3333
() => run('Firebase Cleanup', 'tools-prod cleanupFirebase --execute'),
3434
{
3535
timezone: 'UTC',

0 commit comments

Comments
 (0)