fix(dm): purge DMs with zero live members (exclude-uid, scoped) so dead-both threads clean up (S2)

This commit is contained in:
Jannis Braun
2026-07-02 15:37:33 +02:00
parent e5a1cc9506
commit 12d8256f65
2 changed files with 33 additions and 14 deletions
@@ -56,4 +56,19 @@ describe('tombstoneUser — DM membership partition', () => {
// Survivor's 1-on-1 channel still exists and still has the survivor
expect(testDb.select().from(schema.dmChannels).where(eq(schema.dmChannels.id, 'dm_1on1')).get()).toBeTruthy();
});
it('keeps a Deleted<->Survivor 1-on-1, purges a Deleted<->Deleted 1-on-1', async () => {
const { tombstoneUser } = await import('./userDeletion.js');
seedUser('alreadyDead', { isDeleted: 1 });
seedUser('victim'); seedUser('survivor');
// Survivor thread — must survive
seedDm('dm_live', null); seedMember('dm_live', 'victim'); seedMember('dm_live', 'survivor');
// Both-dead thread — victim + an already-deleted partner → must be purged
seedDm('dm_dead', null); seedMember('dm_dead', 'victim'); seedMember('dm_dead', 'alreadyDead');
tombstoneUser('victim', { purgeContent: false });
expect(testDb.select().from(schema.dmChannels).where(eq(schema.dmChannels.id, 'dm_live')).get()).toBeTruthy();
expect(testDb.select().from(schema.dmChannels).where(eq(schema.dmChannels.id, 'dm_dead')).get()).toBeUndefined();
});
});
+18 -14
View File
@@ -185,20 +185,24 @@ export function tombstoneUser(uid: string, options?: TombstoneOptions): string[]
}
}
// Clean up orphaned DM channels (zero members after our removal) — always runs,
// orphaned channels are unreachable garbage regardless of purge mode
const orphanedDmIds = tx.select({ id: schema.dmChannels.id })
.from(schema.dmChannels)
.all()
.filter(dc => {
const memberCount = tx.select({ id: schema.dmMembers.dmChannelId })
.from(schema.dmMembers)
.where(eq(schema.dmMembers.dmChannelId, dc.id))
.all()
.length;
return memberCount === 0;
})
.map(dc => dc.id);
// Purge DMs that are dead after this deletion: among the channels this user
// was in, those with zero LIVE members. The uid being tombstoned right now
// still reads isDeleted=0 (its row is updated below), so it is excluded
// explicitly — this keeps a Deleted<->Survivor 1-on-1 but purges a
// Deleted<->Deleted one. Scoped to the user's channels: only their
// membership changed here, so only these can newly become dead.
const orphanedDmIds = userDmChannelIds.filter(dmId => {
const liveOthers = tx.select({ userId: schema.dmMembers.userId })
.from(schema.dmMembers)
.innerJoin(schema.users, eq(schema.dmMembers.userId, schema.users.id))
.where(and(
eq(schema.dmMembers.dmChannelId, dmId),
eq(schema.users.isDeleted, 0),
))
.all()
.filter(m => m.userId !== uid);
return liveOthers.length === 0;
});
for (const dmId of orphanedDmIds) {
const msgIds = tx.select({ id: schema.dmMessages.id })