mirror of
https://github.com/xtr-dev/rondevu-client.git
synced 2025-12-15 21:33:23 +00:00
Compare commits
16 Commits
claude/fix
...
v0.18.5
| Author | SHA1 | Date | |
|---|---|---|---|
| e48b3bb17a | |||
| 8f7e15e633 | |||
| fcd0f8ead0 | |||
| 8fd4b249de | |||
| 275c156c64 | |||
| c60a5f332a | |||
|
|
ecd6be7f8a | ||
|
|
e652fdc130 | ||
| 0f469e234d | |||
| 68c3ffb4ea | |||
| 02e8e971be | |||
|
|
1ca9a91056 | ||
|
|
4d90cce9d0 | ||
|
|
f5e202384a | ||
|
|
a21fb04a6f | ||
|
|
6ee1d7b5a9 |
57
.github/workflows/claude-code-review.yml
vendored
Normal file
57
.github/workflows/claude-code-review.yml
vendored
Normal 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
50
.github/workflows/claude.yml
vendored
Normal 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:*)'
|
||||||
|
|
||||||
34
package-lock.json
generated
34
package-lock.json
generated
@@ -1,15 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "@xtr-dev/rondevu-client",
|
"name": "@xtr-dev/rondevu-client",
|
||||||
"version": "0.18.0",
|
"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.0",
|
"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",
|
||||||
@@ -1075,6 +1076,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "25.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.2.tgz",
|
||||||
|
"integrity": "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~7.16.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.48.1",
|
"version": "8.48.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz",
|
||||||
@@ -1990,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",
|
||||||
@@ -2828,6 +2847,15 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "7.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||||
|
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@xtr-dev/rondevu-client",
|
"name": "@xtr-dev/rondevu-client",
|
||||||
"version": "0.18.0",
|
"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",
|
||||||
@@ -41,6 +41,7 @@
|
|||||||
"README.md"
|
"README.md"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/ed25519": "^3.0.0"
|
"@noble/ed25519": "^3.0.0",
|
||||||
|
"eventemitter3": "^5.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export interface Service {
|
|||||||
|
|
||||||
export interface IceCandidate {
|
export interface IceCandidate {
|
||||||
candidate: RTCIceCandidateInit | null
|
candidate: RTCIceCandidateInit | null
|
||||||
|
role: 'offerer' | 'answerer'
|
||||||
createdAt: number
|
createdAt: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* WebRTC peer signaling client
|
* WebRTC peer signaling client
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { Rondevu } from './rondevu.js'
|
export { Rondevu, RondevuError, NetworkError, ValidationError, ConnectionError } from './rondevu.js'
|
||||||
export { RondevuAPI } from './api.js'
|
export { RondevuAPI } from './api.js'
|
||||||
export { RpcBatcher } from './rpc-batcher.js'
|
export { RpcBatcher } from './rpc-batcher.js'
|
||||||
|
|
||||||
@@ -32,7 +32,11 @@ export type {
|
|||||||
ConnectToServiceOptions,
|
ConnectToServiceOptions,
|
||||||
ConnectionContext,
|
ConnectionContext,
|
||||||
OfferContext,
|
OfferContext,
|
||||||
OfferFactory
|
OfferFactory,
|
||||||
|
ActiveOffer,
|
||||||
|
FindServiceOptions,
|
||||||
|
ServiceResult,
|
||||||
|
PaginatedServiceResult
|
||||||
} from './rondevu.js'
|
} from './rondevu.js'
|
||||||
|
|
||||||
export type { CryptoAdapter } from './crypto-adapter.js'
|
export type { CryptoAdapter } from './crypto-adapter.js'
|
||||||
|
|||||||
@@ -81,13 +81,11 @@ export class NodeCryptoAdapter implements CryptoAdapter {
|
|||||||
|
|
||||||
bytesToBase64(bytes: Uint8Array): string {
|
bytesToBase64(bytes: Uint8Array): string {
|
||||||
// Node.js Buffer provides native base64 encoding
|
// Node.js Buffer provides native base64 encoding
|
||||||
// @ts-expect-error - Buffer is available in Node.js but not in browser TypeScript definitions
|
|
||||||
return Buffer.from(bytes).toString('base64')
|
return Buffer.from(bytes).toString('base64')
|
||||||
}
|
}
|
||||||
|
|
||||||
base64ToBytes(base64: string): Uint8Array {
|
base64ToBytes(base64: string): Uint8Array {
|
||||||
// Node.js Buffer provides native base64 decoding
|
// Node.js Buffer provides native base64 decoding
|
||||||
// @ts-expect-error - Buffer is available in Node.js but not in browser TypeScript definitions
|
|
||||||
return new Uint8Array(Buffer.from(base64, 'base64'))
|
return new Uint8Array(Buffer.from(base64, 'base64'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
260
src/rondevu.ts
260
src/rondevu.ts
@@ -1,5 +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 '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'
|
||||||
@@ -100,7 +101,7 @@ export interface ConnectToServiceOptions {
|
|||||||
rtcConfig?: RTCConfiguration // Optional: override default ICE servers
|
rtcConfig?: RTCConfiguration // Optional: override default ICE servers
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ActiveOffer {
|
export interface ActiveOffer {
|
||||||
offerId: string
|
offerId: string
|
||||||
serviceFqn: string
|
serviceFqn: string
|
||||||
pc: RTCPeerConnection
|
pc: RTCPeerConnection
|
||||||
@@ -109,6 +110,73 @@ interface ActiveOffer {
|
|||||||
createdAt: number
|
createdAt: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FindServiceOptions {
|
||||||
|
mode?: 'direct' | 'random' | 'paginated' // Default: 'direct' if serviceFqn has username, 'random' otherwise
|
||||||
|
limit?: number // For paginated mode (default: 10)
|
||||||
|
offset?: number // For paginated mode (default: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServiceResult {
|
||||||
|
serviceId: string
|
||||||
|
username: string
|
||||||
|
serviceFqn: string
|
||||||
|
offerId: string
|
||||||
|
sdp: string
|
||||||
|
createdAt: number
|
||||||
|
expiresAt: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedServiceResult {
|
||||||
|
services: ServiceResult[]
|
||||||
|
count: number
|
||||||
|
limit: number
|
||||||
|
offset: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base error class for Rondevu errors
|
||||||
|
*/
|
||||||
|
export class RondevuError extends Error {
|
||||||
|
constructor(message: string, public context?: Record<string, any>) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'RondevuError'
|
||||||
|
Object.setPrototypeOf(this, RondevuError.prototype)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Network-related errors (API calls, connectivity)
|
||||||
|
*/
|
||||||
|
export class NetworkError extends RondevuError {
|
||||||
|
constructor(message: string, context?: Record<string, any>) {
|
||||||
|
super(message, context)
|
||||||
|
this.name = 'NetworkError'
|
||||||
|
Object.setPrototypeOf(this, NetworkError.prototype)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validation errors (invalid input, malformed data)
|
||||||
|
*/
|
||||||
|
export class ValidationError extends RondevuError {
|
||||||
|
constructor(message: string, context?: Record<string, any>) {
|
||||||
|
super(message, context)
|
||||||
|
this.name = 'ValidationError'
|
||||||
|
Object.setPrototypeOf(this, ValidationError.prototype)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebRTC connection errors (peer connection failures, ICE issues)
|
||||||
|
*/
|
||||||
|
export class ConnectionError extends RondevuError {
|
||||||
|
constructor(message: string, context?: Record<string, any>) {
|
||||||
|
super(message, context)
|
||||||
|
this.name = 'ConnectionError'
|
||||||
|
Object.setPrototypeOf(this, ConnectionError.prototype)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rondevu - Complete WebRTC signaling client
|
* Rondevu - Complete WebRTC signaling client
|
||||||
*
|
*
|
||||||
@@ -163,7 +231,7 @@ interface ActiveOffer {
|
|||||||
* rondevu.stopFilling()
|
* rondevu.stopFilling()
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export class Rondevu {
|
export class Rondevu extends EventEmitter {
|
||||||
// Constants
|
// Constants
|
||||||
private static readonly DEFAULT_TTL_MS = 300000 // 5 minutes
|
private static readonly DEFAULT_TTL_MS = 300000 // 5 minutes
|
||||||
private static readonly POLLING_INTERVAL_MS = 1000 // 1 second
|
private static readonly POLLING_INTERVAL_MS = 1000 // 1 second
|
||||||
@@ -191,6 +259,7 @@ export class Rondevu {
|
|||||||
private filling = false
|
private filling = false
|
||||||
private pollingInterval: ReturnType<typeof setInterval> | null = null
|
private pollingInterval: ReturnType<typeof setInterval> | null = null
|
||||||
private lastPollTimestamp = 0
|
private lastPollTimestamp = 0
|
||||||
|
private isPolling = false // Guard against concurrent poll execution
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
apiUrl: string,
|
apiUrl: string,
|
||||||
@@ -204,6 +273,7 @@ export class Rondevu {
|
|||||||
rtcPeerConnection?: typeof RTCPeerConnection,
|
rtcPeerConnection?: typeof RTCPeerConnection,
|
||||||
rtcIceCandidate?: typeof RTCIceCandidate
|
rtcIceCandidate?: typeof RTCIceCandidate
|
||||||
) {
|
) {
|
||||||
|
super()
|
||||||
this.apiUrl = apiUrl
|
this.apiUrl = apiUrl
|
||||||
this.username = username
|
this.username = username
|
||||||
this.keypair = keypair
|
this.keypair = keypair
|
||||||
@@ -402,6 +472,9 @@ export class Rondevu {
|
|||||||
? event.candidate.toJSON()
|
? event.candidate.toJSON()
|
||||||
: event.candidate
|
: event.candidate
|
||||||
|
|
||||||
|
// Emit local ICE candidate event
|
||||||
|
this.emit('ice:candidate:local', offerId, candidateData)
|
||||||
|
|
||||||
await this.api.addOfferIceCandidates(
|
await this.api.addOfferIceCandidates(
|
||||||
serviceFqn,
|
serviceFqn,
|
||||||
offerId,
|
offerId,
|
||||||
@@ -447,6 +520,11 @@ export class Rondevu {
|
|||||||
? event.candidate.toJSON()
|
? event.candidate.toJSON()
|
||||||
: event.candidate
|
: event.candidate
|
||||||
|
|
||||||
|
// Emit local ICE candidate event
|
||||||
|
if (offerId) {
|
||||||
|
this.emit('ice:candidate:local', offerId, candidateData)
|
||||||
|
}
|
||||||
|
|
||||||
if (offerId) {
|
if (offerId) {
|
||||||
// We have the offerId, send directly
|
// We have the offerId, send directly
|
||||||
try {
|
try {
|
||||||
@@ -499,6 +577,15 @@ export class Rondevu {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.debug(`Offer created: ${offerId}`)
|
this.debug(`Offer created: ${offerId}`)
|
||||||
|
this.emit('offer:created', offerId, serviceFqn)
|
||||||
|
|
||||||
|
// Set up data channel open handler (offerer side)
|
||||||
|
if (dc) {
|
||||||
|
dc.onopen = () => {
|
||||||
|
this.debug(`Data channel opened for offer ${offerId}`)
|
||||||
|
this.emit('connection:opened', offerId, dc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 6. Send any queued early ICE candidates
|
// 6. Send any queued early ICE candidates
|
||||||
if (earlyIceCandidates.length > 0) {
|
if (earlyIceCandidates.length > 0) {
|
||||||
@@ -515,6 +602,7 @@ export class Rondevu {
|
|||||||
this.debug(`Offer ${offerId} connection state: ${pc.connectionState}`)
|
this.debug(`Offer ${offerId} connection state: ${pc.connectionState}`)
|
||||||
|
|
||||||
if (pc.connectionState === 'failed' || pc.connectionState === 'closed') {
|
if (pc.connectionState === 'failed' || pc.connectionState === 'closed') {
|
||||||
|
this.emit('connection:closed', offerId!)
|
||||||
this.activeOffers.delete(offerId!)
|
this.activeOffers.delete(offerId!)
|
||||||
this.fillOffers() // Try to replace failed offer
|
this.fillOffers() // Try to replace failed offer
|
||||||
}
|
}
|
||||||
@@ -547,6 +635,13 @@ export class Rondevu {
|
|||||||
private async pollInternal(): Promise<void> {
|
private async pollInternal(): Promise<void> {
|
||||||
if (!this.filling) return
|
if (!this.filling) return
|
||||||
|
|
||||||
|
// Prevent concurrent poll execution to avoid duplicate answer processing
|
||||||
|
if (this.isPolling) {
|
||||||
|
this.debug('Poll already in progress, skipping')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isPolling = true
|
||||||
try {
|
try {
|
||||||
const result = await this.api.poll(this.lastPollTimestamp)
|
const result = await this.api.poll(this.lastPollTimestamp)
|
||||||
|
|
||||||
@@ -556,16 +651,25 @@ export class Rondevu {
|
|||||||
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
|
|
||||||
|
|
||||||
// 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -577,6 +681,7 @@ export class Rondevu {
|
|||||||
|
|
||||||
for (const item of answererCandidates) {
|
for (const item of answererCandidates) {
|
||||||
if (item.candidate) {
|
if (item.candidate) {
|
||||||
|
this.emit('ice:candidate:remote', offerId, item.candidate, item.role)
|
||||||
await activeOffer.pc.addIceCandidate(new RTCIceCandidate(item.candidate))
|
await activeOffer.pc.addIceCandidate(new RTCIceCandidate(item.candidate))
|
||||||
this.lastPollTimestamp = Math.max(this.lastPollTimestamp, item.createdAt)
|
this.lastPollTimestamp = Math.max(this.lastPollTimestamp, item.createdAt)
|
||||||
}
|
}
|
||||||
@@ -585,6 +690,8 @@ export class Rondevu {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Rondevu] Polling error:', err)
|
console.error('[Rondevu] Polling error:', err)
|
||||||
|
} finally {
|
||||||
|
this.isPolling = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -621,6 +728,7 @@ export class Rondevu {
|
|||||||
stopFilling(): void {
|
stopFilling(): void {
|
||||||
this.debug('Stopping offer filling and polling')
|
this.debug('Stopping offer filling and polling')
|
||||||
this.filling = false
|
this.filling = false
|
||||||
|
this.isPolling = false // Reset polling guard
|
||||||
|
|
||||||
// Stop polling
|
// Stop polling
|
||||||
if (this.pollingInterval) {
|
if (this.pollingInterval) {
|
||||||
@@ -638,6 +746,51 @@ export class Rondevu {
|
|||||||
this.activeOffers.clear()
|
this.activeOffers.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the count of active offers
|
||||||
|
* @returns Number of active offers
|
||||||
|
*/
|
||||||
|
getOfferCount(): number {
|
||||||
|
return this.activeOffers.size
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an offer is currently connected
|
||||||
|
* @param offerId - The offer ID to check
|
||||||
|
* @returns True if the offer exists and has been answered
|
||||||
|
*/
|
||||||
|
isConnected(offerId: string): boolean {
|
||||||
|
const offer = this.activeOffers.get(offerId)
|
||||||
|
return offer ? offer.answered : false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect all active offers
|
||||||
|
* Similar to stopFilling() but doesn't stop the polling/filling process
|
||||||
|
*/
|
||||||
|
async disconnectAll(): Promise<void> {
|
||||||
|
this.debug('Disconnecting all offers')
|
||||||
|
for (const [offerId, offer] of this.activeOffers.entries()) {
|
||||||
|
this.debug(`Closing offer ${offerId}`)
|
||||||
|
offer.dc?.close()
|
||||||
|
offer.pc.close()
|
||||||
|
}
|
||||||
|
this.activeOffers.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current service status
|
||||||
|
* @returns Object with service state information
|
||||||
|
*/
|
||||||
|
getServiceStatus(): { active: boolean; offerCount: number; maxOffers: number; filling: boolean } {
|
||||||
|
return {
|
||||||
|
active: this.currentService !== null,
|
||||||
|
offerCount: this.activeOffers.size,
|
||||||
|
maxOffers: this.maxOffers,
|
||||||
|
filling: this.filling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the full service FQN from various input options
|
* Resolve the full service FQN from various input options
|
||||||
* Supports direct FQN, service+username, or service discovery
|
* Supports direct FQN, service+username, or service discovery
|
||||||
@@ -652,7 +805,7 @@ export class Rondevu {
|
|||||||
} else if (service) {
|
} else if (service) {
|
||||||
// Discovery mode - get random service
|
// Discovery mode - get random service
|
||||||
this.debug(`Discovering service: ${service}`)
|
this.debug(`Discovering service: ${service}`)
|
||||||
const discovered = await this.discoverService(service)
|
const discovered = await this.findService(service) as ServiceResult
|
||||||
return discovered.serviceFqn
|
return discovered.serviceFqn
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Either serviceFqn or service must be provided')
|
throw new Error('Either serviceFqn or service must be provided')
|
||||||
@@ -679,6 +832,7 @@ export class Rondevu {
|
|||||||
)
|
)
|
||||||
for (const item of result.candidates) {
|
for (const item of result.candidates) {
|
||||||
if (item.candidate) {
|
if (item.candidate) {
|
||||||
|
this.emit('ice:candidate:remote', offerId, item.candidate, item.role)
|
||||||
await pc.addIceCandidate(new RTCIceCandidate(item.candidate))
|
await pc.addIceCandidate(new RTCIceCandidate(item.candidate))
|
||||||
lastIceTimestamp = item.createdAt
|
lastIceTimestamp = item.createdAt
|
||||||
}
|
}
|
||||||
@@ -748,6 +902,7 @@ export class Rondevu {
|
|||||||
pc.ondatachannel = (event) => {
|
pc.ondatachannel = (event) => {
|
||||||
this.debug('Data channel received from offerer')
|
this.debug('Data channel received from offerer')
|
||||||
dc = event.channel
|
dc = event.channel
|
||||||
|
this.emit('connection:opened', serviceData.offerId, dc)
|
||||||
resolve(dc)
|
resolve(dc)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -819,56 +974,45 @@ export class Rondevu {
|
|||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get service by FQN (with username) - Direct lookup
|
* Find a service - unified discovery method
|
||||||
* Example: chat:1.0.0@alice
|
*
|
||||||
|
* Replaces getService(), discoverService(), and discoverServices() with a single method.
|
||||||
|
*
|
||||||
|
* @param serviceFqn - Service identifier (e.g., 'chat:1.0.0' or 'chat:1.0.0@alice')
|
||||||
|
* @param options - Discovery options
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* // Direct lookup (has username)
|
||||||
|
* const service = await rondevu.findService('chat:1.0.0@alice')
|
||||||
|
*
|
||||||
|
* // Random discovery (no username)
|
||||||
|
* const service = await rondevu.findService('chat:1.0.0')
|
||||||
|
*
|
||||||
|
* // Paginated discovery
|
||||||
|
* const result = await rondevu.findService('chat:1.0.0', {
|
||||||
|
* mode: 'paginated',
|
||||||
|
* limit: 20,
|
||||||
|
* offset: 0
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
async getService(serviceFqn: string): Promise<{
|
async findService(
|
||||||
serviceId: string
|
serviceFqn: string,
|
||||||
username: string
|
options?: FindServiceOptions
|
||||||
serviceFqn: string
|
): Promise<ServiceResult | PaginatedServiceResult> {
|
||||||
offerId: string
|
const { mode, limit = 10, offset = 0 } = options || {}
|
||||||
sdp: string
|
|
||||||
createdAt: number
|
|
||||||
expiresAt: number
|
|
||||||
}> {
|
|
||||||
return await this.api.getService(serviceFqn)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Auto-detect mode if not specified
|
||||||
* Discover a random available service without knowing the username
|
const hasUsername = serviceFqn.includes('@')
|
||||||
* Example: chat:1.0.0 (without @username)
|
const effectiveMode = mode || (hasUsername ? 'direct' : 'random')
|
||||||
*/
|
|
||||||
async discoverService(serviceVersion: string): Promise<{
|
|
||||||
serviceId: string
|
|
||||||
username: string
|
|
||||||
serviceFqn: string
|
|
||||||
offerId: string
|
|
||||||
sdp: string
|
|
||||||
createdAt: number
|
|
||||||
expiresAt: number
|
|
||||||
}> {
|
|
||||||
return await this.api.getService(serviceVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (effectiveMode === 'paginated') {
|
||||||
* Discover multiple available services with pagination
|
return await this.api.getService(serviceFqn, { limit, offset })
|
||||||
* Example: chat:1.0.0 (without @username)
|
} else {
|
||||||
*/
|
// Both 'direct' and 'random' use the same API call
|
||||||
async discoverServices(serviceVersion: string, limit: number = 10, offset: number = 0): Promise<{
|
return await this.api.getService(serviceFqn)
|
||||||
services: Array<{
|
}
|
||||||
serviceId: string
|
|
||||||
username: string
|
|
||||||
serviceFqn: string
|
|
||||||
offerId: string
|
|
||||||
sdp: string
|
|
||||||
createdAt: number
|
|
||||||
expiresAt: number
|
|
||||||
}>
|
|
||||||
count: number
|
|
||||||
limit: number
|
|
||||||
offset: number
|
|
||||||
}> {
|
|
||||||
return await this.api.getService(serviceVersion, { limit, offset })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
Reference in New Issue
Block a user