Filter stale non-member votes from reveal logic
All checks were successful
Build & Push Container Image / build (push) Successful in 5s
All checks were successful
Build & Push Container Image / build (push) Successful in 5s
revealIfComplete and forceReveal now accept a Set of room member keys and only count votes from current members. Stale votes from users who left the room are stripped before computing averages. emitSessionState also filters votedUserKeys and revealed votes to members only. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2d78b9ff07
commit
36c8c5f6f4
2 changed files with 47 additions and 15 deletions
|
|
@ -24,7 +24,6 @@ import {
|
||||||
joinRoom,
|
joinRoom,
|
||||||
leaveRoom,
|
leaveRoom,
|
||||||
getRoomMembers,
|
getRoomMembers,
|
||||||
getRoomMemberCount,
|
|
||||||
isRoomMember
|
isRoomMember
|
||||||
} from './services/roomService.js';
|
} from './services/roomService.js';
|
||||||
import { updateIssueEstimate } from './services/jiraService.js';
|
import { updateIssueEstimate } from './services/jiraService.js';
|
||||||
|
|
@ -147,19 +146,28 @@ async function emitSessionState(roomId, sessionId, tenantCloudId) {
|
||||||
const snapshot = await getSessionSnapshot(sessionId, tenantCloudId);
|
const snapshot = await getSessionSnapshot(sessionId, tenantCloudId);
|
||||||
if (!snapshot) return;
|
if (!snapshot) return;
|
||||||
|
|
||||||
const memberCount = await getRoomMemberCount(roomId, tenantCloudId);
|
const members = await getRoomMembers(roomId, tenantCloudId);
|
||||||
|
const memberKeys = new Set(members.map((m) => m.userKey));
|
||||||
|
|
||||||
|
// Filter to only votes from current room members
|
||||||
|
const votedUserKeys = snapshot.votedUserKeys.filter((k) => memberKeys.has(k));
|
||||||
|
|
||||||
io.to(`room:${roomId}`).emit('poker:vote-update', {
|
io.to(`room:${roomId}`).emit('poker:vote-update', {
|
||||||
sessionId,
|
sessionId,
|
||||||
voteCount: snapshot.voteCount,
|
voteCount: votedUserKeys.length,
|
||||||
votedUserKeys: snapshot.votedUserKeys,
|
votedUserKeys,
|
||||||
memberCount
|
memberCount: memberKeys.size
|
||||||
});
|
});
|
||||||
|
|
||||||
if (snapshot.session.state === 'REVEALED' || snapshot.session.state === 'SAVED') {
|
if (snapshot.session.state === 'REVEALED' || snapshot.session.state === 'SAVED') {
|
||||||
|
// Filter revealed votes to members only
|
||||||
|
const votes = {};
|
||||||
|
for (const [k, v] of Object.entries(snapshot.votesByUser)) {
|
||||||
|
if (memberKeys.has(k)) votes[k] = v;
|
||||||
|
}
|
||||||
io.to(`room:${roomId}`).emit('poker:revealed', {
|
io.to(`room:${roomId}`).emit('poker:revealed', {
|
||||||
sessionId,
|
sessionId,
|
||||||
votes: snapshot.votesByUser,
|
votes,
|
||||||
average: snapshot.session.averageEstimate,
|
average: snapshot.session.averageEstimate,
|
||||||
suggestedEstimate: snapshot.session.suggestedEstimate,
|
suggestedEstimate: snapshot.session.suggestedEstimate,
|
||||||
savedEstimate: snapshot.session.savedEstimate
|
savedEstimate: snapshot.session.savedEstimate
|
||||||
|
|
@ -279,8 +287,9 @@ io.on('connection', (socket) => {
|
||||||
}
|
}
|
||||||
votesTotal.inc();
|
votesTotal.inc();
|
||||||
|
|
||||||
const memberCount = await getRoomMemberCount(roomId, socket.user.jiraCloudId);
|
const members = await getRoomMembers(roomId, socket.user.jiraCloudId);
|
||||||
const reveal = await revealIfComplete(sessionId, socket.user.jiraCloudId, memberCount);
|
const memberKeys = new Set(members.map((m) => m.userKey));
|
||||||
|
const reveal = await revealIfComplete(sessionId, socket.user.jiraCloudId, memberKeys);
|
||||||
|
|
||||||
await emitSessionState(roomId, sessionId, socket.user.jiraCloudId);
|
await emitSessionState(roomId, sessionId, socket.user.jiraCloudId);
|
||||||
|
|
||||||
|
|
@ -305,13 +314,22 @@ io.on('connection', (socket) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const snapshot = await forceReveal(sessionId, socket.user.jiraCloudId);
|
const pre = await getSessionSnapshot(sessionId, socket.user.jiraCloudId);
|
||||||
|
if (!pre) {
|
||||||
|
socket.emit('poker:error', { error: 'Session not found.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const roomId = pre.session.roomId;
|
||||||
|
|
||||||
|
const members = await getRoomMembers(roomId, socket.user.jiraCloudId);
|
||||||
|
const memberKeys = new Set(members.map((m) => m.userKey));
|
||||||
|
|
||||||
|
const snapshot = await forceReveal(sessionId, socket.user.jiraCloudId, memberKeys);
|
||||||
if (!snapshot) {
|
if (!snapshot) {
|
||||||
socket.emit('poker:error', { error: 'Unable to reveal votes.' });
|
socket.emit('poker:error', { error: 'Unable to reveal votes.' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const roomId = snapshot.session.roomId;
|
|
||||||
io.to(`room:${roomId}`).emit('poker:revealed', {
|
io.to(`room:${roomId}`).emit('poker:revealed', {
|
||||||
sessionId,
|
sessionId,
|
||||||
votes: snapshot.votesByUser,
|
votes: snapshot.votesByUser,
|
||||||
|
|
|
||||||
|
|
@ -167,14 +167,23 @@ export async function submitVote({ sessionId, tenantCloudId, userKey, vote }) {
|
||||||
return getSnapshot(result);
|
return getSnapshot(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function revealIfComplete(sessionId, tenantCloudId, roomMemberCount) {
|
export async function revealIfComplete(sessionId, tenantCloudId, roomMemberKeys) {
|
||||||
const result = await withSessionCas(tenantCloudId, sessionId, (session) => {
|
const result = await withSessionCas(tenantCloudId, sessionId, (session) => {
|
||||||
if (session.state !== 'VOTING') return undefined;
|
if (session.state !== 'VOTING') return undefined;
|
||||||
const allVoted = roomMemberCount > 0 &&
|
|
||||||
session.votes.size === roomMemberCount;
|
// Only count votes from current room members
|
||||||
|
const memberVoteCount = [...session.votes.keys()]
|
||||||
|
.filter((k) => roomMemberKeys.has(k)).length;
|
||||||
|
const allVoted = roomMemberKeys.size > 0 &&
|
||||||
|
memberVoteCount === roomMemberKeys.size;
|
||||||
|
|
||||||
if (!allVoted) return undefined; // no mutation needed
|
if (!allVoted) return undefined; // no mutation needed
|
||||||
|
|
||||||
|
// Strip stale votes from non-members
|
||||||
|
for (const key of session.votes.keys()) {
|
||||||
|
if (!roomMemberKeys.has(key)) session.votes.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
const numericVotes = [...session.votes.values()]
|
const numericVotes = [...session.votes.values()]
|
||||||
.map(Number)
|
.map(Number)
|
||||||
.filter(Number.isFinite);
|
.filter(Number.isFinite);
|
||||||
|
|
@ -207,10 +216,15 @@ export async function revealIfComplete(sessionId, tenantCloudId, roomMemberCount
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function forceReveal(sessionId, tenantCloudId) {
|
export async function forceReveal(sessionId, tenantCloudId, roomMemberKeys) {
|
||||||
const result = await withSessionCas(tenantCloudId, sessionId, (session) => {
|
const result = await withSessionCas(tenantCloudId, sessionId, (session) => {
|
||||||
if (session.state !== 'VOTING') return undefined;
|
if (session.state !== 'VOTING') return undefined;
|
||||||
|
|
||||||
|
// Strip stale votes from non-members
|
||||||
|
for (const key of session.votes.keys()) {
|
||||||
|
if (!roomMemberKeys.has(key)) session.votes.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
const numericVotes = [...session.votes.values()]
|
const numericVotes = [...session.votes.values()]
|
||||||
.map(Number)
|
.map(Number)
|
||||||
.filter(Number.isFinite);
|
.filter(Number.isFinite);
|
||||||
|
|
@ -225,7 +239,7 @@ export async function forceReveal(sessionId, tenantCloudId) {
|
||||||
return session;
|
return session;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result) {
|
if (!result || result.state !== 'REVEALED') {
|
||||||
const current = await getSession(tenantCloudId, sessionId);
|
const current = await getSession(tenantCloudId, sessionId);
|
||||||
if (!current) return null;
|
if (!current) return null;
|
||||||
return getSnapshot(current);
|
return getSnapshot(current);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue