2 Commits

Author SHA1 Message Date
e48b3bb17a v0.18.5 - Fix duplicate answer processing regression 2025-12-14 14:19:44 +01:00
8f7e15e633 Fix duplicate answer processing regression
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 <noreply@anthropic.com>
2025-12-14 14:19:34 +01:00
3 changed files with 20 additions and 12 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@xtr-dev/rondevu-client",
"version": "0.18.4",
"version": "0.18.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@xtr-dev/rondevu-client",
"version": "0.18.4",
"version": "0.18.5",
"license": "MIT",
"dependencies": {
"@noble/ed25519": "^3.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@xtr-dev/rondevu-client",
"version": "0.18.4",
"version": "0.18.5",
"description": "TypeScript client for Rondevu with durable WebRTC connections, automatic reconnection, and message queuing",
"type": "module",
"main": "dist/index.js",

View File

@@ -651,17 +651,25 @@ export class Rondevu extends EventEmitter {
if (activeOffer && !activeOffer.answered) {
this.debug(`Received answer for offer ${answer.offerId}`)
// Mark as answered BEFORE setRemoteDescription to prevent race condition
activeOffer.answered = true
try {
await activeOffer.pc.setRemoteDescription({
type: 'answer',
sdp: answer.sdp
})
activeOffer.answered = true
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
}
}
}