javascript 47 lines · 8 steps

Diffing sets to sync group membership

Compute the minimal add/remove delta between two id lists, then apply only the changes inside a transaction.

Explained by highlit
1export function diffIds(current, desired) {
2 const currentSet = new Set(current);
3 const desiredSet = new Set(desired);
4 
5 const toAdd = [];
6 const toRemove = [];
7 
8 for (const id of desiredSet) {
9 if (!currentSet.has(id)) toAdd.push(id);
10 }
11 
12 for (const id of currentSet) {
13 if (!desiredSet.has(id)) toRemove.push(id);
14 }
15 
16 const unchanged = current.filter((id) => desiredSet.has(id));
17 
18 return {
19 toAdd,
20 toRemove,
21 unchanged,
22 hasChanges: toAdd.length > 0 || toRemove.length > 0,
23 };
24}
25 
26export async function syncMembership(groupId, desiredUserIds) {
27 const existing = await db.groupMembers.findAll({ where: { groupId } });
28 const { toAdd, toRemove, hasChanges } = diffIds(
29 existing.map((m) => m.userId),
30 desiredUserIds,
31 );
32 
33 if (!hasChanges) return { changed: false };
34 
35 await db.transaction(async (tx) => {
36 if (toRemove.length) {
37 await tx.groupMembers.destroy({ where: { groupId, userId: toRemove } });
38 }
39 if (toAdd.length) {
40 await tx.groupMembers.bulkCreate(
41 toAdd.map((userId) => ({ groupId, userId })),
42 );
43 }
44 });
45 
46 return { changed: true, added: toAdd, removed: toRemove };
47}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Wrapping id lists in Sets turns membership checks into O(1) lookups, keeping the diff linear.
  2. 2Computing a delta first lets you skip work entirely when nothing has changed.
  3. 3Applying removes and adds inside one transaction keeps the final state atomic and consistent.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Diffing sets to sync group membership — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code