Implement connection persistence for offerer side through "offer rotation".
When a connection fails, the same OffererConnection object is rebound to a
new offer instead of being destroyed, preserving message buffers and event
listeners.
Features:
- Connection objects persist across disconnections
- Message buffering works seamlessly through rotations
- Event listeners remain active after rotation
- New `connection:rotated` event for tracking offer changes
- Max rotation attempts limit (default: 5) with fallback
Implementation:
- Add OffererConnection.rebindToOffer() method with AsyncLock protection
- Add rotation tracking: rotating flag, rotationAttempts counter
- Add OfferPool.createNewOfferForRotation() helper method
- Modify OfferPool failure handler to rotate instead of destroy
- Add connection:rotated event to OfferPoolEvents interface
- Forward connection:rotated event in Rondevu class
- Add edge case handling for cleanup during rotation
- Reset rotation attempts on successful connection
Documentation:
- Add "Connection Persistence" section to README with examples
- Update "New in v0.20.0" feature list
- Add v0.20.0 changelog entry
- Document rotation benefits and behavior
Benefits:
- Same connection object remains usable through disconnections
- Message buffer preserved during temporary disconnections
- Event listeners don't need to be re-registered
- Simpler user code - no need to track new connections
100% backward compatible - no breaking changes.
🤖 Generated with Claude Code (https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Revert the v0.18.10 revert, going back to EventEmitter API
- Durable WebRTC connections with auto-reconnect and message buffering
- AnswererConnection and OffererConnection classes
- Rich event system with 20+ events
- Updated README with v0.18.11 references and changelog
Remove EventEmitter-based durable connections introduced in v0.18.8/v0.18.9:
- Remove OffererConnection/AnswererConnection classes
- Remove auto-reconnect and message buffering
- Restore callback-based API with offerFactory and onConnection
- Update package description to reflect simpler API
This version returns to the stable v0.18.7 API while keeping all bug fixes.
- Add v0.18.9 features section highlighting durable connections
- Document breaking change: connectToService() returns AnswererConnection
- Update Quick Start examples with correct event-driven API
- Add explicit warnings about waiting for 'connected' event
- Include Connection Configuration and Events documentation
- Add Migration Guide section with upgrade instructions
- Update changelog with v0.18.9 changes
This addresses user issues where messages were buffered instead of sent
due to sending before connection established.
Reverted pollInternal to exactly match v0.18.3 which was the last
working version. The changes in v0.18.5 and v0.18.6 that moved the
answered flag and timestamp updates were causing issues.
v0.18.3 working logic restored:
- Check !activeOffer.answered
- Call setRemoteDescription (no try/catch)
- Set answered = true AFTER
- Update lastPollTimestamp AFTER
- No pre-emptive timestamp updates
The only difference from v0.18.3 is the eventemitter3 import which
should not affect answer processing behavior.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Root cause: When setRemoteDescription failed, lastPollTimestamp was
never updated, causing the server to return the same answer repeatedly.
Solution:
1. Update lastPollTimestamp BEFORE processing answers
2. Calculate max timestamp from all received answers upfront
3. Don't throw on setRemoteDescription errors - just log and continue
4. This ensures we advance the timestamp even if processing fails
This prevents the infinite loop of:
- Poll returns answer
- Processing fails
- Timestamp not updated
- Next poll returns same answer
- Repeat
Now the timestamp advances regardless of processing success,
preventing duplicate answer fetches from the server.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
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>
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>
Add polling guard to prevent concurrent pollInternal() execution.
The setInterval callback doesn't await the async pollInternal(),
which could cause multiple polls to process the same answer before
lastPollTimestamp is updated, resulting in "Called in wrong state:
stable" errors from setRemoteDescription().
- Update README.md example to match new OfferFactory signature
- Add error handling and RTCPeerConnection cleanup on factory failure
- Document setupIceCandidateHandler() method usage
- Use undefined instead of null for offerId variable
Co-authored-by: Bas <bvdaakster@users.noreply.github.com>
Change the OfferFactory signature to receive the RTCPeerConnection as a
parameter instead of rtcConfig. This allows Rondevu to:
1. Create the RTCPeerConnection itself
2. Set up ICE candidate handlers BEFORE the factory runs
3. Ensure no candidates are lost when setLocalDescription() triggers
ICE gathering
This is a cleaner fix for #2 that eliminates the race condition at the
source rather than working around it with queuing.
BREAKING CHANGE: OfferFactory signature changed from
(rtcConfig: RTCConfiguration) => Promise<OfferContext>
to
(pc: RTCPeerConnection) => Promise<OfferContext>
OfferContext no longer includes 'pc' since it's now provided by Rondevu.
Queue ICE candidates that are generated before we have the offerId from
the server. When the factory calls setLocalDescription(), ICE gathering
starts immediately, but we couldn't send candidates until we had the
offerId from publishService(). Now we:
1. Set up a queuing handler immediately after getting the pc from factory
2. Buffer any early candidates while publishing to get the offerId
3. Flush all queued candidates once we have the offerId
4. Continue handling future candidates normally
Fixes#2
Allow users to pass WebRTC polyfills (RTCPeerConnection, RTCIceCandidate) through RondevuOptions instead of manually setting global variables. The client now automatically applies these to globalThis when provided.
This simplifies Node.js integration:
- Before: Users had to manually set globalThis.RTCPeerConnection
- After: Pass rtcPeerConnection and rtcIceCandidate options
Example:
const rondevu = await Rondevu.connect({
apiUrl: 'https://api.example.com',
username: 'alice',
cryptoAdapter: new NodeCryptoAdapter(),
rtcPeerConnection: wrtc.RTCPeerConnection,
rtcIceCandidate: wrtc.RTCIceCandidate
})
Handle both browser and Node.js (wrtc) environments when processing ICE candidates. Browsers provide toJSON() method on candidates, but wrtc library candidates are already plain objects. Check for toJSON() existence before calling it.
Fixes: TypeError: event.candidate.toJSON is not a function in Node.js
Remove all references to NODE_HOST_GUIDE.md and test-connect.js:
- README.md: Removed Node.js Host Guide section and links
- ADVANCED.md: Simplified Node.js example, removed guide references
Keep inline Node.js example code for reference.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Change relative paths to absolute GitHub URLs:
- ../demo/NODE_HOST_GUIDE.md → github.com/xtr-dev/rondevu-demo/blob/main/NODE_HOST_GUIDE.md
- ../demo/test-connect.js → github.com/xtr-dev/rondevu-demo/blob/main/test-connect.js
Affected files:
- README.md (2 links fixed)
- ADVANCED.md (2 links fixed)
Why: Client and demo are separate repositories, so relative paths
don't work when viewing on GitHub. Now links work from anywhere.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Replace `candidate: any` with `candidate: RTCIceCandidateInit | null`:
Changes:
- api.ts poll() return type (line 366)
- rondevu.ts pollOffers() return type (line 827)
- IceCandidate interface (line 41)
Benefits:
- Better type safety and IntelliSense support
- Matches WebRTC spec (candidates can be null)
- Catches potential type errors at compile time
- Clearer API contracts for consumers
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Break down 129-line method into focused helper methods:
New private methods:
- resolveServiceFqn(): Determines full FQN from various input options
Handles direct FQN, service+username, or discovery mode
- startIcePolling(): Manages remote ICE candidate polling
Encapsulates polling logic and interval management
Benefits:
- connectToService() reduced from 129 to ~98 lines
- Each method has single responsibility
- Easier to test and maintain
- Better code readability with clear method names
- Reusable components for future features
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Refactor ICE candidate handler setup:
- Create setupIceCandidateHandler() private method
- Remove duplicate code from createOffer() and connectToService()
- Both methods now call the shared handler setup
Benefits:
- DRY principle: 13 lines of duplicate code eliminated
- Easier to maintain: changes to ICE handling logic only needed in one place
- Consistent error handling across both code paths
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Validation: Add checks for empty strings in connectToService()
- Validates serviceFqn is not empty string
- Validates service is not empty string
- Validates username is not empty string
- Prevents silent failures from whitespace-only inputs
Impact: Better error messages for invalid inputs
Cleanup: Remove Service and ServiceRequest types from imports
- Not used anywhere in rondevu.ts
- Reduces unnecessary dependencies
- Verified: Build passes with no TypeScript errors
- Remove entire RondevuSignaler class documentation section
- Remove PollingConfig interface from Types section
- Delete obsolete USAGE.md file (completely outdated)
- Update all examples to use new automatic API
- Fix migration examples to show current publishService() format