4 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
fcd0f8ead0 0.18.4 2025-12-14 14:06:57 +01:00
8fd4b249de Fix EventEmitter for cross-platform compatibility (v0.18.3)
Replace Node.js 'events' module with 'eventemitter3' package
to ensure compatibility in both browser and Node.js environments.

Changes:
- Replace import from 'events' to 'eventemitter3'
- Add eventemitter3 as dependency
- Remove @types/node (no longer needed)

Fixes browser bundling error where 'events' module was not available.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 14:05:36 +01:00
3 changed files with 36 additions and 18 deletions

20
package-lock.json generated
View File

@@ -1,19 +1,19 @@
{ {
"name": "@xtr-dev/rondevu-client", "name": "@xtr-dev/rondevu-client",
"version": "0.18.3", "version": "0.18.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@xtr-dev/rondevu-client", "name": "@xtr-dev/rondevu-client",
"version": "0.18.3", "version": "0.18.5",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@noble/ed25519": "^3.0.0" "@noble/ed25519": "^3.0.0",
"eventemitter3": "^5.0.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
"@types/node": "^25.0.2",
"@typescript-eslint/eslint-plugin": "^8.48.1", "@typescript-eslint/eslint-plugin": "^8.48.1",
"@typescript-eslint/parser": "^8.48.1", "@typescript-eslint/parser": "^8.48.1",
"eslint": "^9.39.1", "eslint": "^9.39.1",
@@ -1082,6 +1082,8 @@
"integrity": "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==", "integrity": "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
} }
@@ -2001,6 +2003,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"license": "MIT"
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -2844,7 +2852,9 @@
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"optional": true,
"peer": true
}, },
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.2.2", "version": "1.2.2",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@xtr-dev/rondevu-client", "name": "@xtr-dev/rondevu-client",
"version": "0.18.3", "version": "0.18.5",
"description": "TypeScript client for Rondevu with durable WebRTC connections, automatic reconnection, and message queuing", "description": "TypeScript client for Rondevu with durable WebRTC connections, automatic reconnection, and message queuing",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
@@ -25,7 +25,6 @@
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
"@types/node": "^25.0.2",
"@typescript-eslint/eslint-plugin": "^8.48.1", "@typescript-eslint/eslint-plugin": "^8.48.1",
"@typescript-eslint/parser": "^8.48.1", "@typescript-eslint/parser": "^8.48.1",
"eslint": "^9.39.1", "eslint": "^9.39.1",
@@ -42,6 +41,7 @@
"README.md" "README.md"
], ],
"dependencies": { "dependencies": {
"@noble/ed25519": "^3.0.0" "@noble/ed25519": "^3.0.0",
"eventemitter3": "^5.0.1"
} }
} }

View File

@@ -1,6 +1,6 @@
import { RondevuAPI, Keypair, IceCandidate, BatcherOptions } from './api.js' import { RondevuAPI, Keypair, IceCandidate, BatcherOptions } from './api.js'
import { CryptoAdapter } from './crypto-adapter.js' import { CryptoAdapter } from './crypto-adapter.js'
import { EventEmitter } from 'events' import { EventEmitter } from 'eventemitter3'
// ICE server preset names // ICE server preset names
export type IceServerPreset = 'ipv4-turn' | 'hostname-turns' | 'google-stun' | 'relay-only' export type IceServerPreset = 'ipv4-turn' | 'hostname-turns' | 'google-stun' | 'relay-only'
@@ -651,17 +651,25 @@ export class Rondevu extends EventEmitter {
if (activeOffer && !activeOffer.answered) { if (activeOffer && !activeOffer.answered) {
this.debug(`Received answer for offer ${answer.offerId}`) this.debug(`Received answer for offer ${answer.offerId}`)
await activeOffer.pc.setRemoteDescription({ // Mark as answered BEFORE setRemoteDescription to prevent race condition
type: 'answer',
sdp: answer.sdp
})
activeOffer.answered = true activeOffer.answered = true
this.lastPollTimestamp = answer.answeredAt
this.emit('offer:answered', answer.offerId, answer.answererId)
// Create replacement offer try {
this.fillOffers() 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
}
} }
} }