mirror of
https://github.com/xtr-dev/rondevu-client.git
synced 2025-12-13 04:13:25 +00:00
Implement RPC request batching and throttling
Adds automatic request batching to reduce HTTP overhead by combining
multiple RPC calls into a single request.
Features:
- RpcBatcher class for intelligent request batching
- Configurable batch size (default: 10 requests)
- Configurable wait time (default: 50ms)
- Throttling to prevent overwhelming the server (default: 10ms)
- Automatic flushing when batch is full
- Enabled by default, can be disabled via options
Changes:
- Created rpc-batcher.ts with RpcBatcher class
- Updated RondevuAPI to use batcher by default
- Added batching option to RondevuOptions
- Updated README with batching documentation
- Bumped version to 0.16.0
Example usage:
// Default (batching enabled with defaults)
const rondevu = new Rondevu({ apiUrl: 'https://api.ronde.vu' })
// Custom batching settings
const rondevu = new Rondevu({
apiUrl: 'https://api.ronde.vu',
batching: { maxBatchSize: 20, maxWaitTime: 100 }
})
// Disable batching
const rondevu = new Rondevu({
apiUrl: 'https://api.ronde.vu',
batching: false
})
This can reduce HTTP requests by up to 90% during intensive operations
like ICE candidate exchange.
🤖 Generated with Claude Code
https://claude.com/claude-code
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@xtr-dev/rondevu-client",
|
"name": "@xtr-dev/rondevu-client",
|
||||||
"version": "0.15.0",
|
"version": "0.16.0",
|
||||||
"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",
|
||||||
|
|||||||
33
src/api.ts
33
src/api.ts
@@ -4,8 +4,10 @@
|
|||||||
|
|
||||||
import { CryptoAdapter, Keypair } from './crypto-adapter.js'
|
import { CryptoAdapter, Keypair } from './crypto-adapter.js'
|
||||||
import { WebCryptoAdapter } from './web-crypto-adapter.js'
|
import { WebCryptoAdapter } from './web-crypto-adapter.js'
|
||||||
|
import { RpcBatcher, BatcherOptions } from './rpc-batcher.js'
|
||||||
|
|
||||||
export type { Keypair } from './crypto-adapter.js'
|
export type { Keypair } from './crypto-adapter.js'
|
||||||
|
export type { BatcherOptions } from './rpc-batcher.js'
|
||||||
|
|
||||||
export interface OfferRequest {
|
export interface OfferRequest {
|
||||||
sdp: string
|
sdp: string
|
||||||
@@ -65,15 +67,25 @@ interface RpcResponse {
|
|||||||
*/
|
*/
|
||||||
export class RondevuAPI {
|
export class RondevuAPI {
|
||||||
private crypto: CryptoAdapter
|
private crypto: CryptoAdapter
|
||||||
|
private batcher: RpcBatcher | null = null
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private baseUrl: string,
|
private baseUrl: string,
|
||||||
private username: string,
|
private username: string,
|
||||||
private keypair: Keypair,
|
private keypair: Keypair,
|
||||||
cryptoAdapter?: CryptoAdapter
|
cryptoAdapter?: CryptoAdapter,
|
||||||
|
batcherOptions?: BatcherOptions | false
|
||||||
) {
|
) {
|
||||||
// Use WebCryptoAdapter by default (browser environment)
|
// Use WebCryptoAdapter by default (browser environment)
|
||||||
this.crypto = cryptoAdapter || new WebCryptoAdapter()
|
this.crypto = cryptoAdapter || new WebCryptoAdapter()
|
||||||
|
|
||||||
|
// Create batcher if not explicitly disabled
|
||||||
|
if (batcherOptions !== false) {
|
||||||
|
this.batcher = new RpcBatcher(
|
||||||
|
(requests) => this.rpcBatchDirect(requests),
|
||||||
|
batcherOptions
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,9 +106,22 @@ export class RondevuAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute RPC call
|
* Execute RPC call with optional batching
|
||||||
*/
|
*/
|
||||||
private async rpc(request: RpcRequest): Promise<any> {
|
private async rpc(request: RpcRequest): Promise<any> {
|
||||||
|
// Use batcher if enabled
|
||||||
|
if (this.batcher) {
|
||||||
|
return await this.batcher.add(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct call without batching
|
||||||
|
return await this.rpcDirect(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute single RPC call directly (bypasses batcher)
|
||||||
|
*/
|
||||||
|
private async rpcDirect(request: RpcRequest): Promise<any> {
|
||||||
const response = await fetch(`${this.baseUrl}/rpc`, {
|
const response = await fetch(`${this.baseUrl}/rpc`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -117,9 +142,9 @@ export class RondevuAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute batch RPC calls
|
* Execute batch RPC calls directly (bypasses batcher)
|
||||||
*/
|
*/
|
||||||
private async rpcBatch(requests: RpcRequest[]): Promise<any[]> {
|
private async rpcBatchDirect(requests: RpcRequest[]): Promise<any[]> {
|
||||||
const response = await fetch(`${this.baseUrl}/rpc`, {
|
const response = await fetch(`${this.baseUrl}/rpc`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
export { Rondevu } from './rondevu.js'
|
export { Rondevu } from './rondevu.js'
|
||||||
export { RondevuAPI } from './api.js'
|
export { RondevuAPI } from './api.js'
|
||||||
export { RondevuSignaler } from './rondevu-signaler.js'
|
export { RondevuSignaler } from './rondevu-signaler.js'
|
||||||
|
export { RpcBatcher } from './rpc-batcher.js'
|
||||||
|
|
||||||
// Export crypto adapters
|
// Export crypto adapters
|
||||||
export { WebCryptoAdapter } from './web-crypto-adapter.js'
|
export { WebCryptoAdapter } from './web-crypto-adapter.js'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { RondevuAPI, Keypair, Service, ServiceRequest, IceCandidate } from './api.js'
|
import { RondevuAPI, Keypair, Service, ServiceRequest, IceCandidate, BatcherOptions } from './api.js'
|
||||||
import { CryptoAdapter } from './crypto-adapter.js'
|
import { CryptoAdapter } from './crypto-adapter.js'
|
||||||
|
|
||||||
export interface RondevuOptions {
|
export interface RondevuOptions {
|
||||||
@@ -6,6 +6,7 @@ export interface RondevuOptions {
|
|||||||
username?: string // Optional, will generate anonymous if not provided
|
username?: string // Optional, will generate anonymous if not provided
|
||||||
keypair?: Keypair // Optional, will generate if not provided
|
keypair?: Keypair // Optional, will generate if not provided
|
||||||
cryptoAdapter?: CryptoAdapter // Optional, defaults to WebCryptoAdapter
|
cryptoAdapter?: CryptoAdapter // Optional, defaults to WebCryptoAdapter
|
||||||
|
batching?: BatcherOptions | false // Optional, defaults to enabled with default options
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PublishServiceOptions {
|
export interface PublishServiceOptions {
|
||||||
@@ -55,17 +56,20 @@ export class Rondevu {
|
|||||||
private keypair: Keypair | null = null
|
private keypair: Keypair | null = null
|
||||||
private usernameClaimed = false
|
private usernameClaimed = false
|
||||||
private cryptoAdapter?: CryptoAdapter
|
private cryptoAdapter?: CryptoAdapter
|
||||||
|
private batchingOptions?: BatcherOptions | false
|
||||||
|
|
||||||
constructor(options: RondevuOptions) {
|
constructor(options: RondevuOptions) {
|
||||||
this.apiUrl = options.apiUrl
|
this.apiUrl = options.apiUrl
|
||||||
this.username = options.username || this.generateAnonymousUsername()
|
this.username = options.username || this.generateAnonymousUsername()
|
||||||
this.keypair = options.keypair || null
|
this.keypair = options.keypair || null
|
||||||
this.cryptoAdapter = options.cryptoAdapter
|
this.cryptoAdapter = options.cryptoAdapter
|
||||||
|
this.batchingOptions = options.batching
|
||||||
|
|
||||||
console.log('[Rondevu] Constructor called:', {
|
console.log('[Rondevu] Constructor called:', {
|
||||||
username: this.username,
|
username: this.username,
|
||||||
hasKeypair: !!this.keypair,
|
hasKeypair: !!this.keypair,
|
||||||
publicKey: this.keypair?.publicKey
|
publicKey: this.keypair?.publicKey,
|
||||||
|
batchingEnabled: options.batching !== false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,8 +103,14 @@ export class Rondevu {
|
|||||||
console.log('[Rondevu] Using existing keypair, publicKey:', this.keypair.publicKey)
|
console.log('[Rondevu] Using existing keypair, publicKey:', this.keypair.publicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create API instance with username, keypair, and crypto adapter
|
// Create API instance with username, keypair, crypto adapter, and batching options
|
||||||
this.api = new RondevuAPI(this.apiUrl, this.username, this.keypair, this.cryptoAdapter)
|
this.api = new RondevuAPI(
|
||||||
|
this.apiUrl,
|
||||||
|
this.username,
|
||||||
|
this.keypair,
|
||||||
|
this.cryptoAdapter,
|
||||||
|
this.batchingOptions
|
||||||
|
)
|
||||||
console.log('[Rondevu] Created API instance with username:', this.username)
|
console.log('[Rondevu] Created API instance with username:', this.username)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
157
src/rpc-batcher.ts
Normal file
157
src/rpc-batcher.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
/**
|
||||||
|
* RPC Batcher - Throttles and batches RPC requests to reduce HTTP overhead
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface BatcherOptions {
|
||||||
|
/**
|
||||||
|
* Maximum number of requests to batch together
|
||||||
|
* Default: 10
|
||||||
|
*/
|
||||||
|
maxBatchSize?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum time to wait before sending a batch (ms)
|
||||||
|
* Default: 50ms
|
||||||
|
*/
|
||||||
|
maxWaitTime?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimum time between batches (ms)
|
||||||
|
* Default: 10ms
|
||||||
|
*/
|
||||||
|
throttleInterval?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QueuedRequest {
|
||||||
|
request: any
|
||||||
|
resolve: (value: any) => void
|
||||||
|
reject: (error: Error) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batches and throttles RPC requests to optimize network usage
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const batcher = new RpcBatcher(
|
||||||
|
* (requests) => api.rpcBatch(requests),
|
||||||
|
* { maxBatchSize: 10, maxWaitTime: 50 }
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* // These will be batched together if called within maxWaitTime
|
||||||
|
* const result1 = await batcher.add(request1)
|
||||||
|
* const result2 = await batcher.add(request2)
|
||||||
|
* const result3 = await batcher.add(request3)
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class RpcBatcher {
|
||||||
|
private queue: QueuedRequest[] = []
|
||||||
|
private batchTimeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
private lastBatchTime: number = 0
|
||||||
|
private options: Required<BatcherOptions>
|
||||||
|
private sendBatch: (requests: any[]) => Promise<any[]>
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
sendBatch: (requests: any[]) => Promise<any[]>,
|
||||||
|
options?: BatcherOptions
|
||||||
|
) {
|
||||||
|
this.sendBatch = sendBatch
|
||||||
|
this.options = {
|
||||||
|
maxBatchSize: options?.maxBatchSize ?? 10,
|
||||||
|
maxWaitTime: options?.maxWaitTime ?? 50,
|
||||||
|
throttleInterval: options?.throttleInterval ?? 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an RPC request to the batch queue
|
||||||
|
* Returns a promise that resolves when the request completes
|
||||||
|
*/
|
||||||
|
async add(request: any): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.queue.push({ request, resolve, reject })
|
||||||
|
|
||||||
|
// Send immediately if batch is full
|
||||||
|
if (this.queue.length >= this.options.maxBatchSize) {
|
||||||
|
this.flush()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule batch if not already scheduled
|
||||||
|
if (!this.batchTimeout) {
|
||||||
|
this.batchTimeout = setTimeout(() => {
|
||||||
|
this.flush()
|
||||||
|
}, this.options.maxWaitTime)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flush the queue immediately
|
||||||
|
*/
|
||||||
|
async flush(): Promise<void> {
|
||||||
|
// Clear timeout if set
|
||||||
|
if (this.batchTimeout) {
|
||||||
|
clearTimeout(this.batchTimeout)
|
||||||
|
this.batchTimeout = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing to flush
|
||||||
|
if (this.queue.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throttle: wait if we sent a batch too recently
|
||||||
|
const now = Date.now()
|
||||||
|
const timeSinceLastBatch = now - this.lastBatchTime
|
||||||
|
if (timeSinceLastBatch < this.options.throttleInterval) {
|
||||||
|
const waitTime = this.options.throttleInterval - timeSinceLastBatch
|
||||||
|
await new Promise(resolve => setTimeout(resolve, waitTime))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract requests from queue
|
||||||
|
const batch = this.queue.splice(0, this.options.maxBatchSize)
|
||||||
|
const requests = batch.map(item => item.request)
|
||||||
|
|
||||||
|
this.lastBatchTime = Date.now()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Send batch request
|
||||||
|
const results = await this.sendBatch(requests)
|
||||||
|
|
||||||
|
// Resolve individual promises
|
||||||
|
for (let i = 0; i < batch.length; i++) {
|
||||||
|
batch[i].resolve(results[i])
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Reject all promises in batch
|
||||||
|
for (const item of batch) {
|
||||||
|
item.reject(error as Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current queue size
|
||||||
|
*/
|
||||||
|
getQueueSize(): number {
|
||||||
|
return this.queue.length
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the queue without sending
|
||||||
|
*/
|
||||||
|
clear(): void {
|
||||||
|
if (this.batchTimeout) {
|
||||||
|
clearTimeout(this.batchTimeout)
|
||||||
|
this.batchTimeout = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject all pending requests
|
||||||
|
for (const item of this.queue) {
|
||||||
|
item.reject(new Error('Batch queue cleared'))
|
||||||
|
}
|
||||||
|
|
||||||
|
this.queue = []
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user