11 Commits

Author SHA1 Message Date
db28a133bf 0.18.1 2025-12-14 11:04:21 +01:00
Bas
4d90cce9d0 Merge pull request #3 from xtr-dev/claude/fix-issue-2-6zYvD
Fix issue #2
2025-12-14 11:03:47 +01:00
Bas
f5e202384a Merge pull request #4 from xtr-dev/add-claude-github-actions-1765706414732
Add Claude Code GitHub Workflow
2025-12-14 11:01:07 +01:00
Bas
a21fb04a6f "Claude Code Review workflow" 2025-12-14 11:00:16 +01:00
Bas
6ee1d7b5a9 "Claude PR Assistant workflow" 2025-12-14 11:00:15 +01:00
Claude
62a6cdcb99 Update package-lock.json 2025-12-14 09:57:07 +00:00
Claude
febe3b7270 Fix early ICE candidates lost due to late handler setup in createOffer()
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
2025-12-14 09:56:27 +00:00
83831cae77 v0.18.0 - Add WebRTC polyfill support for Node.js 2025-12-13 18:47:38 +01:00
e954a70aa7 Add WebRTC polyfill support for Node.js environments
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
  })
2025-12-13 18:47:27 +01:00
9b879522da v0.17.1 - Fix ICE candidate handling for Node.js compatibility 2025-12-13 15:19:49 +01:00
eb280e3826 Fix ICE candidate handling for Node.js compatibility
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
2025-12-13 15:19:45 +01:00
5 changed files with 182 additions and 21 deletions

View File

@@ -0,0 +1,57 @@
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
prompt: |
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number }}
Please review this pull request and provide feedback on:
- Code quality and best practices
- Potential bugs or issues
- Performance considerations
- Security concerns
- Test coverage
Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'

50
.github/workflows/claude.yml vendored Normal file
View File

@@ -0,0 +1,50 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
# prompt: 'Update the pull request description to include a summary of changes.'
# Optional: Add claude_args to customize behavior and configuration
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr:*)'

16
package-lock.json generated
View File

@@ -1,16 +1,15 @@
{
"name": "@xtr-dev/rondevu-client",
"version": "0.17.0",
"version": "0.18.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@xtr-dev/rondevu-client",
"version": "0.17.0",
"version": "0.18.1",
"license": "MIT",
"dependencies": {
"@noble/ed25519": "^3.0.0",
"@xtr-dev/rondevu-client": "^0.9.2"
"@noble/ed25519": "^3.0.0"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
@@ -1310,15 +1309,6 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@xtr-dev/rondevu-client": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@xtr-dev/rondevu-client/-/rondevu-client-0.9.2.tgz",
"integrity": "sha512-DVow5AOPU40dqQtlfQK7J2GNX8dz2/4UzltMqublaPZubbkRYgocvp0b76NQu5F6v150IstMV2N49uxAYqogVw==",
"license": "MIT",
"dependencies": {
"@noble/ed25519": "^3.0.0"
}
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",

View File

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

View File

@@ -57,6 +57,9 @@ export interface RondevuOptions {
batching?: BatcherOptions | false // Optional, defaults to enabled with default options
iceServers?: IceServerPreset | RTCIceServer[] // Optional: preset name or custom STUN/TURN servers
debug?: boolean // Optional: enable debug logging (default: false)
// WebRTC polyfills for Node.js environments (e.g., wrtc)
rtcPeerConnection?: typeof RTCPeerConnection
rtcIceCandidate?: typeof RTCIceCandidate
}
export interface OfferContext {
@@ -167,6 +170,8 @@ export class Rondevu {
private batchingOptions?: BatcherOptions | false
private iceServers: RTCIceServer[]
private debugEnabled: boolean
private rtcPeerConnection?: typeof RTCPeerConnection
private rtcIceCandidate?: typeof RTCIceCandidate
// Service management
private currentService: string | null = null
@@ -188,7 +193,9 @@ export class Rondevu {
iceServers: RTCIceServer[],
cryptoAdapter?: CryptoAdapter,
batchingOptions?: BatcherOptions | false,
debugEnabled = false
debugEnabled = false,
rtcPeerConnection?: typeof RTCPeerConnection,
rtcIceCandidate?: typeof RTCIceCandidate
) {
this.apiUrl = apiUrl
this.username = username
@@ -198,6 +205,8 @@ export class Rondevu {
this.cryptoAdapter = cryptoAdapter
this.batchingOptions = batchingOptions
this.debugEnabled = debugEnabled
this.rtcPeerConnection = rtcPeerConnection
this.rtcIceCandidate = rtcIceCandidate
this.debug('Instance created:', {
username: this.username,
@@ -230,6 +239,14 @@ export class Rondevu {
static async connect(options: RondevuOptions): Promise<Rondevu> {
const username = options.username || Rondevu.generateAnonymousUsername()
// Apply WebRTC polyfills to global scope if provided (Node.js environments)
if (options.rtcPeerConnection) {
globalThis.RTCPeerConnection = options.rtcPeerConnection as any
}
if (options.rtcIceCandidate) {
globalThis.RTCIceCandidate = options.rtcIceCandidate as any
}
// Handle preset string or custom array
let iceServers: RTCIceServer[]
if (typeof options.iceServers === 'string') {
@@ -277,7 +294,9 @@ export class Rondevu {
iceServers,
options.cryptoAdapter,
options.batching,
options.debug || false
options.debug || false,
options.rtcPeerConnection,
options.rtcIceCandidate
)
}
@@ -365,10 +384,17 @@ export class Rondevu {
pc.onicecandidate = async (event) => {
if (event.candidate) {
try {
// Handle both browser and Node.js (wrtc) environments
// Browser: candidate.toJSON() exists
// Node.js wrtc: candidate is already a plain object
const candidateData = typeof event.candidate.toJSON === 'function'
? event.candidate.toJSON()
: event.candidate
await this.api.addOfferIceCandidates(
serviceFqn,
offerId,
[event.candidate.toJSON()]
[candidateData]
)
} catch (err) {
console.error('[Rondevu] Failed to send ICE candidate:', err)
@@ -392,11 +418,42 @@ export class Rondevu {
this.debug('Creating new offer...')
// Create the offer using the factory
// Note: The factory may call setLocalDescription() which triggers ICE gathering
const { pc, dc, offer } = await this.offerFactory(rtcConfig)
// Auto-append username to service
const serviceFqn = `${this.currentService}@${this.username}`
// Queue to buffer ICE candidates generated before we have the offerId
// This fixes the race condition where ICE candidates are lost because
// they're generated before we can set up the handler with the offerId
const earlyIceCandidates: RTCIceCandidateInit[] = []
let offerId: string | null = null
// Set up a queuing ICE candidate handler immediately after getting the pc
// This captures any candidates that fire before we have the offerId
pc.onicecandidate = async (event) => {
if (event.candidate) {
// Handle both browser and Node.js (wrtc) environments
const candidateData = typeof event.candidate.toJSON === 'function'
? event.candidate.toJSON()
: event.candidate
if (offerId) {
// We have the offerId, send directly
try {
await this.api.addOfferIceCandidates(serviceFqn, offerId, [candidateData])
} catch (err) {
console.error('[Rondevu] Failed to send ICE candidate:', err)
}
} else {
// Queue for later - we don't have the offerId yet
this.debug('Queuing early ICE candidate')
earlyIceCandidates.push(candidateData)
}
}
}
// Publish to server
const result = await this.api.publishService({
serviceFqn,
@@ -406,7 +463,7 @@ export class Rondevu {
message: '',
})
const offerId = result.offers[0].offerId
offerId = result.offers[0].offerId
// Store active offer
this.activeOffers.set(offerId, {
@@ -420,15 +477,22 @@ export class Rondevu {
this.debug(`Offer created: ${offerId}`)
// Set up ICE candidate handler
this.setupIceCandidateHandler(pc, serviceFqn, offerId)
// Send any queued early ICE candidates
if (earlyIceCandidates.length > 0) {
this.debug(`Sending ${earlyIceCandidates.length} early ICE candidates`)
try {
await this.api.addOfferIceCandidates(serviceFqn, offerId, earlyIceCandidates)
} catch (err) {
console.error('[Rondevu] Failed to send early ICE candidates:', err)
}
}
// Monitor connection state
pc.onconnectionstatechange = () => {
this.debug(`Offer ${offerId} connection state: ${pc.connectionState}`)
if (pc.connectionState === 'failed' || pc.connectionState === 'closed') {
this.activeOffers.delete(offerId)
this.activeOffers.delete(offerId!)
this.fillOffers() // Try to replace failed offer
}
}