From 8f7e15e6330527d8649cc49d3429236497afa81c Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Sun, 14 Dec 2025 14:19:34 +0100 Subject: [PATCH] Fix duplicate answer processing regression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The activeOffer.answered flag was being set AFTER the async setRemoteDescription() call, creating a race condition window where the same answer could be processed multiple times. Root cause: - Check `!activeOffer.answered` happens - setRemoteDescription() starts (async operation) - Before it completes, another check could happen - Same answer gets processed twice → "stable" state error Fix: - Set activeOffer.answered = true BEFORE setRemoteDescription - Add try/catch to reset flag if setRemoteDescription fails - This prevents duplicate processing while allowing retries on error This regression was introduced when the answered flag assignment was not moved along with other polling logic changes. Fixes: #6 regression 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/rondevu.ts | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/rondevu.ts b/src/rondevu.ts index ba0b6b2..afeb403 100644 --- a/src/rondevu.ts +++ b/src/rondevu.ts @@ -651,17 +651,25 @@ export class Rondevu extends EventEmitter { if (activeOffer && !activeOffer.answered) { this.debug(`Received answer for offer ${answer.offerId}`) - await activeOffer.pc.setRemoteDescription({ - type: 'answer', - sdp: answer.sdp - }) - + // Mark as answered BEFORE setRemoteDescription to prevent race condition activeOffer.answered = true - this.lastPollTimestamp = answer.answeredAt - this.emit('offer:answered', answer.offerId, answer.answererId) - // Create replacement offer - this.fillOffers() + try { + await activeOffer.pc.setRemoteDescription({ + type: 'answer', + sdp: answer.sdp + }) + + this.lastPollTimestamp = answer.answeredAt + this.emit('offer:answered', answer.offerId, answer.answererId) + + // Create replacement offer + this.fillOffers() + } catch (err) { + // If setRemoteDescription fails, reset the answered flag + activeOffer.answered = false + throw err + } } }