45 Commits

Author SHA1 Message Date
77d657e59f Remove Node.js Host Guide
Remove NODE_HOST_GUIDE.md as requested.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 23:24:47 +01:00
b89e16bc0d Remove dead code: App-old.jsx and unused components
Cleanup:
- Delete App-old.jsx (1,368 lines) - outdated pre-refactor version
- Delete entire src/components/ directory (10 unused files)
  - QRScanner.jsx, QRCodeDisplay.jsx, Message.jsx
  - MethodSelector.jsx, FileUploadProgress.jsx, ChatView.jsx
  - ActionSelector.jsx, Header.jsx, TopicsList.jsx, ConnectionForm.jsx

Impact: Removes ~1,500 lines of dead code
Verified: No imports found, build passes
2025-12-12 22:50:29 +01:00
10673c7b62 Update NODE_HOST_GUIDE to use automatic API
- Replace manual WebRTC setup with offerFactory pattern
- Use publishService() + startFilling() instead of manual polling
- Update browser client to use connectToService()
- Remove all manual ICE candidate polling code
- Simplify shutdown to just call stopFilling()
- Reduce example from ~400 lines to ~150 lines
2025-12-12 22:44:04 +01:00
7af4362b53 Remove RondevuSignaler usage and simplify test-connect.js
- Use connectToService() for automatic connection setup
- Remove manual WebRTC setup (~120 lines reduced to ~60 lines)
- Much simpler and cleaner code

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 22:33:41 +01:00
056c027083 Update test-connect.js to use Rondevu.connect() and ICE presets
- Replace new Rondevu() + initialize() with Rondevu.connect()
- Use 'ipv4-turn' ICE server preset
- Use rondevu.addOfferIceCandidates() directly (no getAPIPublic())

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 22:21:58 +01:00
08bee1a6f7 Fix missing fqn variable in handleHostService
- Extract serviceFqn from publishResult for ICE candidate operations
- Fixes "ReferenceError: fqn is not defined" error

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 22:19:50 +01:00
9b5b35ef7d Update to use Rondevu.connect() and ICE server presets
- Replace new Rondevu() + initialize() with Rondevu.connect()
- Use rtcPreset state variable for iceServers option
- Update NODE_HOST_GUIDE.md examples to use presets and new API

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 22:13:13 +01:00
b9c07aeb5a Update for simplified client API and add Node.js hosting guide
Changes:
- Update App.jsx to use Rondevu.connect() instead of new + initialize()
- Update publishService to use 'service' parameter instead of 'serviceFqn'
- Remove explicit claimUsername() calls (now implicit)
- Add comprehensive NODE_HOST_GUIDE.md for hosting WebRTC services in Node.js
- Update README with Node.js hosting section

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 21:57:11 +01:00
7747f59060 Fix: Use ondatachannel instead of createDataChannel for answerer
CRITICAL BUG FIX: As the answerer, we should NOT create our own data channel.
The host (offerer) creates the channel, and we receive it via ondatachannel event.

This was causing messages to be sent on a different channel than the one
the host was listening to, so no messages were being received.

Changes:
- Remove pc.createDataChannel() call
- Add pc.ondatachannel event handler
- Wrap data channel setup in setupDataChannel() function
- Called when channel is received from host
2025-12-12 21:35:34 +01:00
d7caa81042 Add data channel state monitoring and increase send delay
- Increase delay to 500ms before sending identify
- Monitor channel state before sending
- Log bufferedAmount after send
- Add onclose handler for debugging
2025-12-12 21:32:08 +01:00
ac4826e92f Add delay and more debugging to identify message sending 2025-12-12 21:28:21 +01:00
1e46cef35f Add extensive debugging to message handler 2025-12-12 21:27:24 +01:00
9e0728f74a Implement demo message protocol in test script
- Send 'identify' message on connection
- Wait for 'identify_ack' acknowledgment
- Send 'message' type with text for chat
- Keep connection open 5s to receive responses
2025-12-12 21:26:01 +01:00
778fa2e3a9 Fix: Manually serialize ICE candidates for wrtc compatibility
wrtc library doesn't have toJSON() method on RTCIceCandidate.
Manually extract candidate properties instead.
2025-12-12 21:23:40 +01:00
8bb951a91c Fix: Set up ICE handlers before setLocalDescription
- Move onicecandidate handler setup before setLocalDescription
- Directly use API for sending candidates instead of signaler
- Use signaler only for receiving remote candidates
2025-12-12 21:22:39 +01:00
833bf7e519 Fix: Use same TURN config as demo (IPv4-based)
Update test script to use 57.129.61.67 instead of turn.share.fish
to match the demo's default 'ipv4-turn' preset.
2025-12-12 21:16:35 +01:00
508f050f6e Fix: Correct wrtc import for ES modules
The wrtc package needs to access default export when using dynamic import.
2025-12-12 21:13:51 +01:00
cf13672d85 Update test script to use same API URL and service version as demo
- Use api.ronde.vu instead of rondevu.xtrdev.workers.dev
- Use chat:2.0.0 instead of chat:1.0.0 to match demo
2025-12-12 21:10:50 +01:00
37eabff69c Update client package to latest version with batching and auto-claim 2025-12-12 21:06:36 +01:00
758fb2d4ec Improve test script error handling for missing wrtc
Better error message when wrtc is not installed, with clear
instructions on how to install it.

🤖 Generated with Claude Code
https://claude.com/claude-code

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 20:42:08 +01:00
4d95ddcd59 Move wrtc to optionalDependencies
wrtc requires native compilation and may fail on some systems.
Make it optional so the demo can build without it. Users who want
to run the Node.js test script can install it separately.

🤖 Generated with Claude Code
https://claude.com/claude-code

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 20:39:43 +01:00
023be0ab67 Add Node.js test script for connecting to @bas
Adds test-connect.js script that:
- Connects to production Rondevu API
- Discovers chat service from @bas
- Establishes WebRTC connection using wrtc
- Sends 'hello' message via data channel

Usage: npm test

Requires:
- Node.js 19+ (or 18 with --experimental-global-webcrypto)
- wrtc package for WebRTC in Node.js

🤖 Generated with Claude Code
https://claude.com/claude-code

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 20:34:41 +01:00
8959dd6616 Update client dependency for implicit username claiming
🤖 Generated with Claude Code
https://claude.com/claude-code

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 20:22:37 +01:00
5345ebb72f refactor: Update to use poll() instead of pollOffers() 2025-12-12 19:13:23 +01:00
d6b441e352 fix: Show claim UI instead of loading screen for new users
Changed loading condition from checking rondevu existence to checking setupStep.
New users now see the claim UI immediately instead of being stuck on Loading.
2025-12-12 12:29:35 +01:00
5789fc636b fix: Create Rondevu instance in handleClaimUsername for new users
The previous change broke the claim flow for new users because handleClaimUsername
expected rondevu to exist, but we stopped creating it during initialization for
new users. Now handleClaimUsername creates the Rondevu instance itself.
2025-12-12 12:05:45 +01:00
830f411291 feat: Prefill random username without auto-claiming
- Added generateRandomUsername() function
- Prefill username input with random suggestion
- Only create Rondevu instance for returning users with saved credentials
- Changed placeholder to "Choose a username"
- Users must explicitly click "Claim Username" button

This prevents automatic username claiming that was polluting the database
with anonymous users on every page load.
2025-12-12 12:02:06 +01:00
0640afc32c Revert "feat: Add connection pool status display"
This reverts commit a0c5ae18b4.
2025-12-10 22:42:00 +01:00
a0c5ae18b4 feat: Add connection pool status display
- Show real-time status of each host connection in the pool
- Status indicators: Waiting (gray), Answer received (orange), Connecting (blue), Connected (green)
- Visual feedback shows when offerer is waiting for answerer acceptance
- Connection pool counter shows active/total connections
- Helps users understand the connection lifecycle
2025-12-10 22:40:34 +01:00
00c1c21e6c feat: Unified chat UX - auto-accept connections on both sides
- Remove pending requests approval flow
- Auto-accept and open chat immediately when someone connects (same UX as initiating)
- Remove accept/decline buttons and pending requests UI
- Simplified flow: connection -> chat opens automatically
- Both offerer and answerer now have consistent UX
2025-12-10 22:34:20 +01:00
cd93226ea1 Update demo for unified Ed25519 authentication
- Remove all credential storage from localStorage
- Remove 'register' setup step (now: init → claim → ready)
- Update initialization to work without credentials
- Simplify localStorage to only username and keypair
- Anonymous users auto-claim during initialize()
- Named users manually claim username

localStorage keys:
- Keep: rondevu-username, rondevu-keypair, rondevu-contacts
- Remove: rondevu-chat-credentials

Setup flow:
- Load username/keypair from localStorage
- Create Rondevu instance (auto-generates anon username if none saved)
- Call initialize() (no register call)
- Check if username claimed
- Show claim UI if needed, or proceed to ready
2025-12-10 22:07:26 +01:00
2d7a88ba5f Fix close button styling and add disconnect for incoming chats
Close Button Improvements:
- Change pause icon (⏸) to close icon (✕) for clarity
- Change orange background to red (#dc3545) to indicate destructive action
- Update tooltip from 'Close chat' to 'End chat'
- Now visually distinct from the trash icon for removing friends

Add Disconnect for Incoming Chats:
- Add ✕ disconnect button to 'Active Chats' section (non-friends)
- Allows host to disconnect from incoming connections
- Same red styling as friend chat close button
- Shows 'Disconnected from {username}' toast

Both buttons now clearly indicate their purpose and work consistently
across friends and incoming chats.
2025-12-10 20:43:19 +01:00
55e197a5c5 Add connection request approval flow and improve friends list UX
Connection Request Approval:
- Add 'Connection Requests' section showing pending incoming connections
- Host must explicitly accept or deny incoming connection requests
- Shows username before accepting (no auto-accept)
- Accept: moves to active chats and sends acknowledgment
- Decline: closes connection and removes from pending
- Toast notification when someone wants to chat

Friends List UX Improvements:
- Add ⏸ (pause) button to close active chat without removing friend
- Change ✕ to 🗑 (trash) icon for removing friends
- Add confirmation dialog before removing a friend
- Separate 'close chat' from 'remove friend' actions
- Clearer visual distinction between actions

This prevents accidental friend removal and gives hosts control over
who they connect with before establishing the chat.
2025-12-10 20:36:27 +01:00
a08dd1dccc Add incoming chats UI and stop ICE polling when connected
UI improvements:
- Add 'Incoming Chats' section showing connections from non-friends
- Auto-select and show incoming chat when someone connects
- Toast notification when someone connects to you as host

ICE polling fix:
- Stop ICE candidate polling once connection is established
- Prevents unnecessary network requests after connection succeeds
- Only poll while in 'connecting' state

This fixes:
1. Host couldn't see incoming chats (they weren't in UI)
2. Answerer kept polling ICE candidates even after connected
2025-12-10 20:25:38 +01:00
249d1366d3 Fix ICE candidate timing issue: Buffer candidates before offerId is available
CRITICAL FIX: ICE gathering starts immediately when setLocalDescription() is
called, but we didn't have offerIds yet to send them to the server. This meant
all ICE candidates were lost before we could send them.

Solution:
- Attach temporary ICE handler BEFORE setLocalDescription()
- Buffer all ICE candidates in array until we have offerId
- After publishing and receiving offerIds, send all buffered candidates
- Replace handler with permanent one for any future candidates

This fixes the root cause of answerers not receiving any offerer candidates.
2025-12-10 20:17:02 +01:00
91b845aa1c Add detailed logging for answerer ICE candidate exchange
- Log when answerer starts polling for offerer ICE candidates
- Log count of candidates received from offerer
- Log each candidate being added with details
- Log success/failure of adding each candidate
- Log when answerer sends their own ICE candidates to server
- Helps debug ICE candidate exchange on answerer side
2025-12-10 20:06:11 +01:00
db651d4193 Filter ICE candidates by role in offerer polling
- Offerer now filters for answerer ICE candidates only
- Ignores own candidates returned from server
- Uses role field to distinguish between offerer and answerer candidates
- Improves logging to show answerer candidate count
2025-12-10 19:51:54 +01:00
a6329c8708 Implement combined polling and ICE candidate exchange for host
- Add offerId to RTCPeerConnection mapping for answer processing
- Setup ICE candidate handlers on offerer peer connections
- Replace answer-only polling with combined pollOffers() endpoint
- Process both answers and ICE candidates in single poll operation
- Track last poll timestamp to avoid reprocessing old data
- Send offerer ICE candidates to server via addOfferIceCandidates()
- Reduces HTTP requests and completes bidirectional ICE exchange
2025-12-10 19:33:31 +01:00
538315c51f Implement answer polling for offerer side
Host now polls for answered offers every 2 seconds using the new
efficient batch endpoint. When an answer is received, the host
automatically sets the remote description to complete the WebRTC
connection.

Changes:
- Store offerId to RTCPeerConnection mapping when publishing
- Poll for answered offers with timestamp filtering
- Automatically handle incoming answers
- Track last answer timestamp to avoid reprocessing

This completes the bidirectional WebRTC signaling flow.
2025-12-10 19:20:34 +01:00
6a24514e7b Fix signature validation issues in demo
- Add comprehensive logging for init and publish flows
- Verify username is claimed before publishing service
- Detect keypair mismatches and provide clear error messages
- Handle authentication errors more gracefully
- Auto-claim username if not claimed during publish
- Improved user guidance for common errors
2025-12-09 22:54:31 +01:00
46f0eb2e7a Restore full-featured chat UI with contact management, multiple chats, and RTC presets
- Restored contact management (add/remove friends)
- Restored multiple concurrent chat support
- Restored RTC configuration presets (ipv4-turn, hostname-turns, google-stun, relay-only, custom)
- Restored settings modal
- Restored dark theme styling
- Restored online status indicators
- Adapted all features to work with new unified Rondevu API
- Manual RTCPeerConnection management instead of ServiceHost/ServiceClient
- Manual offer pooling (10 concurrent connections)
- Manual ICE candidate polling
2025-12-09 22:49:11 +01:00
9e26ed3b66 Update to use local client package for development
- Install client from local path instead of npm registry
- Workaround for npm registry propagation delay

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-09 22:40:41 +01:00
7835ebd35d v2.1.0: Rewrite to use simplified Rondevu API
- Complete rewrite to use low-level Rondevu API directly
- Removed ServiceHost/ServiceClient abstractions
- Manual RTCPeerConnection and data channel setup
- Custom polling for answers and ICE candidates
- Updated to use new Rondevu class instead of RondevuService
- Direct signaling method calls instead of getAPI()
- Reduced from 926 lines to 542 lines (42% reduction)
- Demonstrates complete WebRTC flow with clear offerer/answerer roles

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-09 22:23:04 +01:00
5f223356ba feat: improve login persistence with server verification
- Properly await isUsernameClaimed() check during initialization
- Verify saved username is still valid on the server
- Show 'Welcome back' toast when restoring session
- Handle expired usernames gracefully
2025-12-08 21:39:58 +01:00
ab55a96fac fix: add missing ServiceHost and ServiceClient imports
- Import ServiceHost and ServiceClient from @xtr-dev/rondevu-client
- Fixes ReferenceError when starting hosting
2025-12-08 21:37:03 +01:00
17 changed files with 2012 additions and 2724 deletions

View File

@@ -296,6 +296,15 @@ await peer.createOffer({
- **RTCDataChannel** - P2P messaging - **RTCDataChannel** - P2P messaging
- **QRCode** - QR code generation for easy topic sharing - **QRCode** - QR code generation for easy topic sharing
## Node.js Service Hosting
Want to create a Node.js service that browser clients can connect to? See:
- **[NODE_HOST_GUIDE.md](NODE_HOST_GUIDE.md)** - Complete guide to hosting WebRTC services in Node.js
- **[test-connect.js](test-connect.js)** - Working example of a Node.js client
- **[TEST_README.md](TEST_README.md)** - Instructions for running the test client
Perfect for creating chat bots, data processors, game servers, or any service that browsers can connect to via WebRTC!
## License ## License
MIT MIT

67
TEST_README.md Normal file
View File

@@ -0,0 +1,67 @@
# Running Node.js Tests
The `test-connect.js` script demonstrates connecting to a Rondevu service from Node.js and sending a WebRTC data channel message.
## Requirements
- Node.js 19+ (or Node.js 18 with `--experimental-global-webcrypto` flag)
- wrtc package (for WebRTC support in Node.js)
## Installation
The `wrtc` package requires native compilation. Due to build complexities, it's not included as a regular dependency.
### Install wrtc manually:
```bash
# Install build tools (if not already installed)
# On Ubuntu/Debian:
sudo apt-get install build-essential python3
# On macOS:
xcode-select --install
# Install wrtc
npm install wrtc
```
**Note:** Installation may take several minutes as it compiles native code.
### Alternative: Test without WebRTC
If wrtc installation fails, you can still test the signaling layer without actual WebRTC connections by modifying the test script or using the browser demo at https://ronde.vu
## Running the Test
Once wrtc is installed:
```bash
npm test
```
This will:
1. Connect to the production Rondevu server
2. Look for @bas's chat service
3. Establish a WebRTC connection
4. Send "hello" via data channel
## Troubleshooting
### wrtc installation fails
Try installing dependencies:
```bash
npm install node-pre-gyp node-gyp
npm install wrtc
```
### "crypto.subtle is not available"
You need Node.js 19+ or run with:
```bash
node --experimental-global-webcrypto test-connect.js
```
### Can't find @bas's service
The test looks for `chat:1.0.0@bas`. If @bas is not online or the service expired, the test will fail. You can modify the `TARGET_USER` constant to test with a different user.

99
package-lock.json generated
View File

@@ -8,12 +8,13 @@
"name": "rondevu-demo", "name": "rondevu-demo",
"version": "2.0.0", "version": "2.0.0",
"dependencies": { "dependencies": {
"@xtr-dev/rondevu-client": "^0.12.0", "@xtr-dev/rondevu-client": "file:../client",
"@zxing/library": "^0.21.3", "@zxing/library": "^0.21.3",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hot-toast": "^2.6.0" "react-hot-toast": "^2.6.0",
"wrtc": "^0.4.7"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.0", "@types/react": "^18.2.0",
@@ -22,6 +23,27 @@
"vite": "^5.4.11" "vite": "^5.4.11"
} }
}, },
"../client": {
"name": "@xtr-dev/rondevu-client",
"version": "0.16.0",
"license": "MIT",
"dependencies": {
"@noble/ed25519": "^3.0.0"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@typescript-eslint/eslint-plugin": "^8.48.1",
"@typescript-eslint/parser": "^8.48.1",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-unicorn": "^62.0.0",
"globals": "^16.5.0",
"prettier": "^3.7.4",
"typescript": "^5.9.3",
"vite": "^7.2.6"
}
},
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.27.1", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@@ -745,15 +767,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@noble/ed25519": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-3.0.0.tgz",
"integrity": "sha512-QyteqMNm0GLqfa5SoYbSC3+Pvykwpn95Zgth4MFVSMKBB75ELl9tX1LAVsN4c3HXOrakHsF2gL4zWDAYCcsnzg==",
"license": "MIT",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@rolldown/pluginutils": { "node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27", "version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
@@ -1171,13 +1184,8 @@
} }
}, },
"node_modules/@xtr-dev/rondevu-client": { "node_modules/@xtr-dev/rondevu-client": {
"version": "0.12.0", "resolved": "../client",
"resolved": "https://registry.npmjs.org/@xtr-dev/rondevu-client/-/rondevu-client-0.12.0.tgz", "link": true
"integrity": "sha512-c1UecF29Cjck7h7b7KWyCti8YSVVkuvEzyAz7aaFwAYBEumgX1r143mTBRfAMQkFL4upkG/PL5bvBGRY9QCpug==",
"license": "MIT",
"dependencies": {
"@noble/ed25519": "^3.0.0"
}
}, },
"node_modules/@zxing/library": { "node_modules/@zxing/library": {
"version": "0.21.3", "version": "0.21.3",
@@ -1226,9 +1234,9 @@
} }
}, },
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.9.3", "version": "2.9.7",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.3.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.7.tgz",
"integrity": "sha512-8QdH6czo+G7uBsNo0GiUfouPN1lRzKdJTGnKXwe12gkFbnnOUaUKGN55dMkfy+mnxmvjwl9zcI4VncczcVXDhA==", "integrity": "sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
@@ -1279,9 +1287,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001759", "version": "1.0.30001760",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz",
"integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==", "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -1374,10 +1382,21 @@
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/domexception": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz",
"integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==",
"deprecated": "Use your platform's native DOMException instead",
"license": "MIT",
"optional": true,
"dependencies": {
"webidl-conversions": "^4.0.2"
}
},
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.266", "version": "1.5.267",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.266.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
"integrity": "sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==", "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@@ -1977,6 +1996,13 @@
} }
} }
}, },
"node_modules/webidl-conversions": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
"integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==",
"license": "BSD-2-Clause",
"optional": true
},
"node_modules/which-module": { "node_modules/which-module": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
@@ -1997,6 +2023,25 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/wrtc": {
"version": "0.4.7",
"resolved": "https://registry.npmjs.org/wrtc/-/wrtc-0.4.7.tgz",
"integrity": "sha512-P6Hn7VT4lfSH49HxLHcHhDq+aFf/jd9dPY7lDHeFhZ22N3858EKuwm2jmnlPzpsRGEPaoF6XwkcxY5SYnt4f/g==",
"bundleDependencies": [
"node-pre-gyp"
],
"hasInstallScript": true,
"license": "BSD-2-Clause",
"dependencies": {
"node-pre-gyp": "^0.13.0"
},
"engines": {
"node": "^8.11.2 || >=10.0.0"
},
"optionalDependencies": {
"domexception": "^1.0.1"
}
},
"node_modules/y18n": { "node_modules/y18n": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",

View File

@@ -7,15 +7,17 @@
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"deploy": "npm run build && npx wrangler pages deploy dist --project-name=rondevu-demo" "deploy": "npm run build && npx wrangler pages deploy dist --project-name=rondevu-demo",
"test": "node test-connect.js"
}, },
"dependencies": { "dependencies": {
"@xtr-dev/rondevu-client": "^0.12.0", "@xtr-dev/rondevu-client": "file:../client",
"@zxing/library": "^0.21.3", "@zxing/library": "^0.21.3",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hot-toast": "^2.6.0" "react-hot-toast": "^2.6.0",
"wrtc": "^0.4.7"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.0", "@types/react": "^18.2.0",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,37 +0,0 @@
import QRScanner from './QRScanner';
function ActionSelector({ action, onSelectAction, onScanComplete, onScanCancel, log }) {
return (
<div className="step-container">
<h2>Chat Demo</h2>
<div className="button-grid button-grid-three">
<button
className="action-button"
onClick={() => onSelectAction('create')}
>
<div className="button-title">Create</div>
<div className="button-description">Start a new connection</div>
</button>
<button
className="action-button"
onClick={() => onSelectAction('join')}
>
<div className="button-title">Join</div>
<div className="button-description">Connect to existing peers</div>
</button>
<button
className="action-button"
onClick={() => onSelectAction('scan')}
>
<div className="button-title">Scan QR</div>
<div className="button-description">Scan a connection code</div>
</button>
</div>
{action === 'scan' && (
<QRScanner onScan={onScanComplete} onCancel={onScanCancel} log={log} />
)}
</div>
);
}
export default ActionSelector;

View File

@@ -1,99 +0,0 @@
import { useRef } from 'react';
import Message from './Message';
import FileUploadProgress from './FileUploadProgress';
function ChatView({
connectedPeer,
currentConnectionId,
messages,
messageInput,
setMessageInput,
channelReady,
logs,
fileUploadProgress,
onSendMessage,
onFileSelect,
onDisconnect,
onDownloadFile,
onCancelUpload
}) {
const fileInputRef = useRef(null);
return (
<div className="chat-container">
<div className="chat-header">
<div>
<h2>Connected</h2>
<p className="connection-details">
Peer: {connectedPeer || 'Unknown'} ID: {currentConnectionId}
</p>
</div>
<button className="disconnect-button" onClick={onDisconnect}>Disconnect</button>
</div>
<div className="messages">
{messages.length === 0 ? (
<p className="empty">No messages yet. Start chatting!</p>
) : (
messages.map((msg, idx) => (
<Message key={idx} message={msg} onDownload={onDownloadFile} />
))
)}
</div>
{fileUploadProgress && (
<FileUploadProgress
fileName={fileUploadProgress.fileName}
progress={fileUploadProgress.progress}
onCancel={onCancelUpload}
/>
)}
<div className="message-input">
<input
ref={fileInputRef}
type="file"
onChange={onFileSelect}
style={{ display: 'none' }}
/>
<button
className="file-button"
onClick={() => fileInputRef.current?.click()}
disabled={!channelReady || fileUploadProgress}
title="Send file"
>
📎
</button>
<input
type="text"
value={messageInput}
onChange={(e) => setMessageInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && onSendMessage()}
placeholder="Type a message..."
disabled={!channelReady}
/>
<button
onClick={onSendMessage}
disabled={!channelReady}
>
Send
</button>
</div>
{logs.length > 0 && (
<details className="logs">
<summary>Activity Log ({logs.length})</summary>
<div className="log-entries">
{logs.map((log, idx) => (
<div key={idx} className={`log-entry ${log.type}`}>
[{log.timestamp}] {log.message}
</div>
))}
</div>
</details>
)}
</div>
);
}
export default ChatView;

View File

@@ -1,53 +0,0 @@
import QRCodeDisplay from './QRCodeDisplay';
function ConnectionForm({
action,
connectionId,
setConnectionId,
connectionStatus,
qrCodeUrl,
currentConnectionId,
onConnect,
onBack
}) {
return (
<div className="step-container">
<h2>{action === 'create' ? 'Create Connection' : 'Join Connection'}</h2>
<div className="form-container">
<div className="form-group">
<label>Connection ID {action === 'create' && '(optional)'}</label>
<input
type="text"
value={connectionId}
onChange={(e) => setConnectionId(e.target.value)}
placeholder={action === 'create' ? 'Auto-generated if empty' : 'Enter connection ID'}
autoFocus={action === 'connect'}
/>
{action === 'create' && !connectionId && (
<p className="help-text">Leave empty to auto-generate a random ID</p>
)}
</div>
<div className="button-row">
<button className="back-button" onClick={onBack}> Back</button>
<button
className="primary-button"
onClick={onConnect}
disabled={
connectionStatus === 'connecting' ||
(action === 'connect' && !connectionId)
}
>
{connectionStatus === 'connecting' ? 'Connecting...' : (action === 'create' ? 'Create' : 'Connect')}
</button>
</div>
{qrCodeUrl && connectionStatus === 'connecting' && action === 'create' && (
<QRCodeDisplay qrCodeUrl={qrCodeUrl} connectionId={currentConnectionId} />
)}
</div>
</div>
);
}
export default ConnectionForm;

View File

@@ -1,17 +0,0 @@
function FileUploadProgress({ fileName, progress, onCancel }) {
return (
<div className="file-upload-progress">
<div className="file-upload-header">
<span className="file-upload-name">{fileName}</span>
<button className="file-upload-cancel" onClick={onCancel}>×</button>
</div>
<div className="progress-bar">
<div className="progress-bar-fill" style={{ width: `${progress}%` }}>
<span className="progress-text">{progress}%</span>
</div>
</div>
</div>
);
}
export default FileUploadProgress;

View File

@@ -1,32 +0,0 @@
function Header() {
return (
<header className="header">
<div className="header-content">
<h1>Rondevu</h1>
<p className="tagline">Simple WebRTC peer signaling and discovery. Meet peers by topic, peer ID, or connection ID.</p>
<div className="header-links">
<a href="https://github.com/xtr-dev/rondevu-client" target="_blank" rel="noopener noreferrer">
<svg className="github-icon" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
</svg>
Client
</a>
<a href="https://github.com/xtr-dev/rondevu-server" target="_blank" rel="noopener noreferrer">
<svg className="github-icon" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
</svg>
Server
</a>
<a href="https://github.com/xtr-dev/rondevu-demo" target="_blank" rel="noopener noreferrer">
<svg className="github-icon" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
</svg>
Demo
</a>
</div>
</div>
</header>
);
}
export default Header;

View File

@@ -1,28 +0,0 @@
function Message({ message, onDownload }) {
const isFile = message.messageType === 'file';
return (
<div className={`message ${message.type}`}>
{isFile ? (
<div className="message-file">
<div className="file-icon">📎</div>
<div className="file-info">
<div className="file-name">{message.file.name}</div>
<div className="file-size">{(message.file.size / 1024).toFixed(2)} KB</div>
</div>
<button
className="file-download"
onClick={() => onDownload(message.file)}
>
Download
</button>
</div>
) : (
<div className="message-text">{message.text}</div>
)}
<div className="message-time">{message.timestamp.toLocaleTimeString()}</div>
</div>
);
}
export default Message;

View File

@@ -1,39 +0,0 @@
function MethodSelector({ action, onSelectMethod, onBack }) {
return (
<div className="step-container">
<h2>{action === 'create' ? 'Create' : 'Join'} by...</h2>
<div className="button-grid">
<button
className="action-button"
onClick={() => onSelectMethod('topic')}
>
<div className="button-title">Topic</div>
<div className="button-description">
{action === 'create' ? 'Create in a topic' : 'Auto-connect to first peer'}
</div>
</button>
{action === 'join' && (
<button
className="action-button"
onClick={() => onSelectMethod('peer-id')}
>
<div className="button-title">Peer ID</div>
<div className="button-description">Connect to specific peer</div>
</button>
)}
<button
className="action-button"
onClick={() => onSelectMethod('connection-id')}
>
<div className="button-title">Connection ID</div>
<div className="button-description">
{action === 'create' ? 'Custom connection code' : 'Direct connection'}
</div>
</button>
</div>
<button className="back-button" onClick={onBack}> Back</button>
</div>
);
}
export default MethodSelector;

View File

@@ -1,13 +0,0 @@
function QRCodeDisplay({ qrCodeUrl, connectionId }) {
if (!qrCodeUrl) return null;
return (
<div className="qr-code-container">
<p className="qr-label">Scan to connect:</p>
<img src={qrCodeUrl} alt="Connection QR Code" className="qr-code" />
<p className="connection-id-display">{connectionId}</p>
</div>
);
}
export default QRCodeDisplay;

View File

@@ -1,74 +0,0 @@
import { useRef, useEffect } from 'react';
import { BrowserQRCodeReader } from '@zxing/library';
function QRScanner({ onScan, onCancel, log }) {
const videoRef = useRef(null);
const scannerRef = useRef(null);
useEffect(() => {
startScanning();
return () => {
stopScanning();
};
}, []);
const startScanning = async () => {
try {
scannerRef.current = new BrowserQRCodeReader();
log('Starting QR scanner...', 'info');
const videoInputDevices = await scannerRef.current.listVideoInputDevices();
if (videoInputDevices.length === 0) {
log('No camera found', 'error');
return;
}
// Prefer back camera (environment-facing)
let selectedDeviceId = videoInputDevices[0].deviceId;
const backCamera = videoInputDevices.find(device =>
device.label.toLowerCase().includes('back') ||
device.label.toLowerCase().includes('rear') ||
device.label.toLowerCase().includes('environment')
);
if (backCamera) {
selectedDeviceId = backCamera.deviceId;
log('Using back camera', 'info');
} else {
log('Back camera not found, using default', 'info');
}
scannerRef.current.decodeFromVideoDevice(
selectedDeviceId,
videoRef.current,
(result, err) => {
if (result) {
const scannedId = result.getText();
log(`Scanned: ${scannedId}`, 'success');
stopScanning();
onScan(scannedId);
}
}
);
} catch (error) {
log(`Scanner error: ${error.message}`, 'error');
}
};
const stopScanning = () => {
if (scannerRef.current) {
scannerRef.current.reset();
log('Scanner stopped', 'info');
}
};
return (
<div className="scanner-container">
<video ref={videoRef} className="scanner-video" />
<button className="back-button" onClick={onCancel}> Cancel</button>
</div>
);
}
export default QRScanner;

View File

@@ -1,120 +0,0 @@
import { useState, useEffect } from 'react';
function TopicsList({ rdv, onClose }) {
const [topics, setTopics] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [page, setPage] = useState(1);
const [pagination, setPagination] = useState(null);
const [limit] = useState(20);
useEffect(() => {
loadTopics();
}, [page]);
const loadTopics = async () => {
setLoading(true);
setError(null);
try {
const response = await rdv.api.listTopics(page, limit);
setTopics(response.topics);
setPagination(response.pagination);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
const handleRefresh = () => {
loadTopics();
};
const handlePrevPage = () => {
if (page > 1) {
setPage(page - 1);
}
};
const handleNextPage = () => {
if (pagination?.hasMore) {
setPage(page + 1);
}
};
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content topics-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>Active Topics</h2>
<button className="close-button" onClick={onClose}>×</button>
</div>
<div className="modal-body">
{error && (
<div className="error-message" style={{ marginBottom: '1rem' }}>
Error: {error}
</div>
)}
{loading ? (
<div className="loading-message">Loading topics...</div>
) : (
<>
{topics.length === 0 ? (
<div className="empty-message">
No active topics found. Be the first to create one!
</div>
) : (
<div className="topics-list">
{topics.map((topic) => (
<div key={topic.topic} className="topic-item">
<div className="topic-name">{topic.topic}</div>
<div className="topic-count">
{topic.count} {topic.count === 1 ? 'peer' : 'peers'}
</div>
</div>
))}
</div>
)}
{pagination && (
<div className="pagination">
<button
onClick={handlePrevPage}
disabled={page === 1}
className="pagination-button"
>
Previous
</button>
<span className="pagination-info">
Page {pagination.page} of {Math.ceil(pagination.total / pagination.limit)}
{' '}({pagination.total} total)
</span>
<button
onClick={handleNextPage}
disabled={!pagination.hasMore}
className="pagination-button"
>
Next
</button>
</div>
)}
</>
)}
</div>
<div className="modal-footer">
<button onClick={handleRefresh} className="button button-secondary">
🔄 Refresh
</button>
<button onClick={onClose} className="button button-primary">
Close
</button>
</div>
</div>
</div>
);
}
export default TopicsList;

146
test-connect.js Normal file
View File

@@ -0,0 +1,146 @@
#!/usr/bin/env node
/**
* Test script to connect to @bas's chat service and send a "hello" message
*
* IMPORTANT: This script requires the 'wrtc' package which must be installed separately.
* See TEST_README.md for detailed installation instructions.
*
* Quick start:
* npm install wrtc
* npm test
*
* Requirements:
* - Node.js 19+ (or Node.js 18 with --experimental-global-webcrypto)
* - wrtc package (requires native compilation)
* - Build tools (python, make, g++)
*/
import { Rondevu, NodeCryptoAdapter } from '@xtr-dev/rondevu-client'
// Import wrtc
let wrtc
try {
const wrtcModule = await import('wrtc')
wrtc = wrtcModule.default || wrtcModule
} catch (error) {
console.error('❌ Error: wrtc package not found or failed to load')
console.error('\nThe wrtc package is required for WebRTC support in Node.js.')
console.error('Install it with:')
console.error('\n npm install wrtc')
console.error('\nNote: wrtc requires native compilation and may take a few minutes to install.')
console.error('\nError details:', error.message)
process.exit(1)
}
const { RTCPeerConnection } = wrtc
// Configuration
const API_URL = 'https://api.ronde.vu'
const TARGET_USER = 'bas'
const SERVICE_FQN = `chat:2.0.0@${TARGET_USER}`
const MESSAGE = 'hello'
async function main() {
console.log('🚀 Rondevu Test Script')
console.log('='.repeat(50))
try {
// 1. Connect to Rondevu with Node crypto adapter and ICE preset
console.log('1. Connecting to Rondevu...')
const rondevu = await Rondevu.connect({
apiUrl: API_URL,
username: `test-${Date.now()}`, // Anonymous test user
cryptoAdapter: new NodeCryptoAdapter(),
iceServers: 'ipv4-turn' // Use ICE server preset
})
console.log(` ✓ Connected as: ${rondevu.getUsername()}`)
console.log(` ✓ Public key: ${rondevu.getPublicKey()?.substring(0, 20)}...`)
// 2. Connect to service (automatic setup)
console.log(`\n2. Connecting to service: ${SERVICE_FQN}`)
let identified = false
const connection = await rondevu.connectToService({
serviceFqn: SERVICE_FQN,
onConnection: ({ dc, peerUsername }) => {
console.log(`✅ Connected to @${peerUsername}`)
// Set up message handler
dc.addEventListener('message', (event) => {
console.log(`📥 RAW DATA:`, event.data)
try {
const msg = JSON.parse(event.data)
console.log(`📥 Parsed message:`, JSON.stringify(msg, null, 2))
if (msg.type === 'identify_ack' && !identified) {
identified = true
console.log(`✅ Connection acknowledged by @${msg.from}`)
// Now send the actual chat message
console.log(`📤 Sending chat message: "${MESSAGE}"`)
dc.send(JSON.stringify({
type: 'message',
text: MESSAGE
}))
// Keep connection open longer to see if we get a response
setTimeout(() => {
console.log('\n✅ Test completed successfully!')
connection.dc.close()
connection.pc.close()
process.exit(0)
}, 5000)
} else if (msg.type === 'message') {
console.log(`💬 @${msg.from || 'peer'}: ${msg.text}`)
} else {
console.log(`📥 Unknown message type: ${msg.type}`)
}
} catch (err) {
console.log(`📥 Parse error:`, err.message)
console.log(`📥 Raw data was:`, event.data)
}
})
// Send identify message after channel opens
console.log(`📤 Sending identify message...`)
const identifyMsg = JSON.stringify({
type: 'identify',
from: rondevu.getUsername()
})
dc.send(identifyMsg)
console.log(` ✓ Identify message sent`)
}
})
// Monitor connection state
connection.pc.onconnectionstatechange = () => {
console.log(` Connection state: ${connection.pc.connectionState}`)
if (connection.pc.connectionState === 'failed') {
console.error('❌ Connection failed')
process.exit(1)
}
}
connection.pc.oniceconnectionstatechange = () => {
console.log(` ICE state: ${connection.pc.iceConnectionState}`)
}
console.log('\n⏳ Waiting for messages...')
// Timeout after 30 seconds
setTimeout(() => {
if (connection.pc.connectionState !== 'connected') {
console.error('❌ Connection timeout')
process.exit(1)
}
}, 30000)
} catch (error) {
console.error('\n❌ Error:', error.message)
console.error(error)
process.exit(1)
}
}
main()