@xtr-dev/rondevu-client 0.18.10 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -117
- package/dist/api/batcher.d.ts +83 -0
- package/dist/api/batcher.js +155 -0
- package/dist/api/client.d.ts +198 -0
- package/dist/api/client.js +400 -0
- package/dist/{answerer-connection.d.ts → connections/answerer.d.ts} +25 -8
- package/dist/{answerer-connection.js → connections/answerer.js} +70 -48
- package/dist/{connection.d.ts → connections/base.d.ts} +30 -7
- package/dist/{connection.js → connections/base.js} +65 -14
- package/dist/connections/config.d.ts +51 -0
- package/dist/{connection-config.js → connections/config.js} +20 -0
- package/dist/{connection-events.d.ts → connections/events.d.ts} +6 -6
- package/dist/connections/offerer.d.ts +108 -0
- package/dist/connections/offerer.js +306 -0
- package/dist/core/ice-config.d.ts +35 -0
- package/dist/core/ice-config.js +111 -0
- package/dist/core/index.d.ts +22 -0
- package/dist/core/index.js +22 -0
- package/dist/core/offer-pool.d.ts +113 -0
- package/dist/core/offer-pool.js +281 -0
- package/dist/core/peer.d.ts +155 -0
- package/dist/core/peer.js +252 -0
- package/dist/core/polling-manager.d.ts +71 -0
- package/dist/core/polling-manager.js +122 -0
- package/dist/core/rondevu-errors.d.ts +59 -0
- package/dist/core/rondevu-errors.js +75 -0
- package/dist/core/rondevu-types.d.ts +125 -0
- package/dist/core/rondevu-types.js +6 -0
- package/dist/core/rondevu.d.ts +296 -0
- package/dist/core/rondevu.js +472 -0
- package/dist/crypto/adapter.d.ts +53 -0
- package/dist/crypto/node.d.ts +57 -0
- package/dist/crypto/node.js +149 -0
- package/dist/crypto/web.d.ts +38 -0
- package/dist/crypto/web.js +129 -0
- package/dist/utils/async-lock.d.ts +42 -0
- package/dist/utils/async-lock.js +75 -0
- package/dist/{message-buffer.d.ts → utils/message-buffer.d.ts} +1 -1
- package/dist/{message-buffer.js → utils/message-buffer.js} +4 -4
- package/dist/webrtc/adapter.d.ts +22 -0
- package/dist/webrtc/adapter.js +5 -0
- package/dist/webrtc/browser.d.ts +12 -0
- package/dist/webrtc/browser.js +15 -0
- package/dist/webrtc/node.d.ts +32 -0
- package/dist/webrtc/node.js +32 -0
- package/package.json +20 -9
- package/dist/api.d.ts +0 -146
- package/dist/api.js +0 -279
- package/dist/connection-config.d.ts +0 -21
- package/dist/crypto-adapter.d.ts +0 -37
- package/dist/index.d.ts +0 -13
- package/dist/index.js +0 -10
- package/dist/node-crypto-adapter.d.ts +0 -35
- package/dist/node-crypto-adapter.js +0 -78
- package/dist/offerer-connection.d.ts +0 -54
- package/dist/offerer-connection.js +0 -177
- package/dist/rondevu-signaler.d.ts +0 -112
- package/dist/rondevu-signaler.js +0 -401
- package/dist/rondevu.d.ts +0 -407
- package/dist/rondevu.js +0 -847
- package/dist/rpc-batcher.d.ts +0 -61
- package/dist/rpc-batcher.js +0 -111
- package/dist/web-crypto-adapter.d.ts +0 -16
- package/dist/web-crypto-adapter.js +0 -52
- /package/dist/{connection-events.js → connections/events.js} +0 -0
- /package/dist/{types.d.ts → core/types.d.ts} +0 -0
- /package/dist/{types.js → core/types.js} +0 -0
- /package/dist/{crypto-adapter.js → crypto/adapter.js} +0 -0
- /package/dist/{exponential-backoff.d.ts → utils/exponential-backoff.d.ts} +0 -0
- /package/dist/{exponential-backoff.js → utils/exponential-backoff.js} +0 -0
package/README.md
CHANGED
|
@@ -2,28 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@xtr-dev/rondevu-client)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**WebRTC signaling client with durable connections**
|
|
6
6
|
|
|
7
|
-
TypeScript
|
|
8
|
-
|
|
9
|
-
**Related repositories:**
|
|
10
|
-
- [@xtr-dev/rondevu-client](https://github.com/xtr-dev/rondevu-client) - TypeScript client library ([npm](https://www.npmjs.com/package/@xtr-dev/rondevu-client))
|
|
11
|
-
- [@xtr-dev/rondevu-server](https://github.com/xtr-dev/rondevu-server) - HTTP signaling server ([npm](https://www.npmjs.com/package/@xtr-dev/rondevu-server), [live](https://api.ronde.vu))
|
|
12
|
-
- [@xtr-dev/rondevu-demo](https://github.com/xtr-dev/rondevu-demo) - Interactive demo ([live](https://ronde.vu))
|
|
13
|
-
|
|
14
|
-
---
|
|
7
|
+
TypeScript client for [Rondevu](https://github.com/xtr-dev/rondevu-server), providing WebRTC signaling with automatic reconnection, message buffering, and tags-based discovery.
|
|
15
8
|
|
|
16
9
|
## Features
|
|
17
10
|
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
22
|
-
- **Efficient Batch Polling**: Single endpoint for answers and ICE candidates (50% fewer requests)
|
|
23
|
-
- **Semantic Version Matching**: Compatible version resolution (chat:1.0.0 matches any 1.x.x)
|
|
24
|
-
- **TypeScript**: Full type safety and autocomplete
|
|
25
|
-
- **Keypair Management**: Generate or reuse Ed25519 keypairs
|
|
26
|
-
- **Automatic Signatures**: All authenticated requests signed automatically
|
|
11
|
+
- **Simple Peer API**: Connect with `rondevu.peer({ tags, username })`
|
|
12
|
+
- **Tags-Based Discovery**: Find peers using tags (e.g., `["chat", "video"]`)
|
|
13
|
+
- **Automatic Reconnection**: Built-in exponential backoff
|
|
14
|
+
- **Message Buffering**: Queues messages during disconnections
|
|
27
15
|
|
|
28
16
|
## Installation
|
|
29
17
|
|
|
@@ -33,144 +21,131 @@ npm install @xtr-dev/rondevu-client
|
|
|
33
21
|
|
|
34
22
|
## Quick Start
|
|
35
23
|
|
|
36
|
-
### Publishing a Service (Offerer)
|
|
37
|
-
|
|
38
24
|
```typescript
|
|
39
25
|
import { Rondevu } from '@xtr-dev/rondevu-client'
|
|
40
26
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
iceServers: 'ipv4-turn' // Preset: 'ipv4-turn', 'hostname-turns', 'google-stun', 'relay-only'
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
// 2. Publish service with automatic offer management
|
|
49
|
-
await rondevu.publishService({
|
|
50
|
-
service: 'chat:1.0.0',
|
|
51
|
-
maxOffers: 5, // Maintain up to 5 concurrent offers
|
|
52
|
-
offerFactory: async (pc) => {
|
|
53
|
-
// pc is created by Rondevu with ICE handlers already attached
|
|
54
|
-
const dc = pc.createDataChannel('chat')
|
|
55
|
-
|
|
56
|
-
dc.addEventListener('open', () => {
|
|
57
|
-
console.log('Connection opened!')
|
|
58
|
-
dc.send('Hello from Alice!')
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
dc.addEventListener('message', (e) => {
|
|
62
|
-
console.log('Received:', e.data)
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
const offer = await pc.createOffer()
|
|
66
|
-
await pc.setLocalDescription(offer)
|
|
67
|
-
return { dc, offer }
|
|
68
|
-
}
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
// 3. Start accepting connections
|
|
72
|
-
await rondevu.startFilling()
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
### Connecting to a Service (Answerer)
|
|
27
|
+
// ============================================
|
|
28
|
+
// ALICE: Host and wait for connections
|
|
29
|
+
// ============================================
|
|
30
|
+
const alice = await Rondevu.connect({ username: 'alice' })
|
|
76
31
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const rondevu = await Rondevu.connect({
|
|
82
|
-
apiUrl: 'https://api.ronde.vu',
|
|
83
|
-
username: 'bob',
|
|
84
|
-
iceServers: 'ipv4-turn'
|
|
32
|
+
alice.on('connection:opened', (offerId, connection) => {
|
|
33
|
+
console.log('Connected to', connection.peerUsername)
|
|
34
|
+
connection.on('message', (data) => console.log('Received:', data))
|
|
35
|
+
connection.send('Hello!')
|
|
85
36
|
})
|
|
86
37
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
serviceFqn: 'chat:1.0.0@alice',
|
|
90
|
-
onConnection: ({ dc, peerUsername }) => {
|
|
91
|
-
console.log('Connected to', peerUsername)
|
|
38
|
+
const offer = await alice.offer({ tags: ['chat'], maxOffers: 5 })
|
|
39
|
+
// Later: offer.cancel() to stop accepting connections
|
|
92
40
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
41
|
+
// ============================================
|
|
42
|
+
// BOB: Connect to Alice
|
|
43
|
+
// ============================================
|
|
44
|
+
const bob = await Rondevu.connect()
|
|
96
45
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
46
|
+
const peer = await bob.peer({
|
|
47
|
+
username: 'alice',
|
|
48
|
+
tags: ['chat']
|
|
101
49
|
})
|
|
102
50
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
connection.pc.close() // Close when done
|
|
51
|
+
peer.on('open', () => peer.send('Hello Alice!'))
|
|
52
|
+
peer.on('message', (data) => console.log('Received:', data))
|
|
106
53
|
```
|
|
107
54
|
|
|
108
|
-
##
|
|
55
|
+
## API Reference
|
|
109
56
|
|
|
110
57
|
### Rondevu.connect()
|
|
111
58
|
|
|
112
59
|
```typescript
|
|
113
60
|
const rondevu = await Rondevu.connect({
|
|
114
|
-
apiUrl
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
iceServers?: IceServerPreset | RTCIceServer[], //
|
|
118
|
-
debug?: boolean
|
|
61
|
+
apiUrl?: string, // Default: 'https://api.ronde.vu'
|
|
62
|
+
credential?: Credential, // Reuse existing credential
|
|
63
|
+
username?: string, // Claim username (4-32 chars)
|
|
64
|
+
iceServers?: IceServerPreset | RTCIceServer[], // Default: 'rondevu'
|
|
65
|
+
debug?: boolean
|
|
119
66
|
})
|
|
67
|
+
|
|
68
|
+
rondevu.getName() // Get username
|
|
69
|
+
rondevu.getCredential() // Get credential for reuse
|
|
120
70
|
```
|
|
121
71
|
|
|
122
|
-
|
|
72
|
+
**ICE Presets**: `'rondevu'` (default), `'rondevu-relay'`, `'google-stun'`, `'public-stun'`
|
|
73
|
+
|
|
74
|
+
### rondevu.peer()
|
|
123
75
|
|
|
124
76
|
```typescript
|
|
125
|
-
await rondevu.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
ttl?: number // Optional: offer lifetime in ms (default: 300000)
|
|
77
|
+
const peer = await rondevu.peer({
|
|
78
|
+
tags: string[],
|
|
79
|
+
username?: string,
|
|
80
|
+
rtcConfig?: RTCConfiguration
|
|
130
81
|
})
|
|
131
82
|
|
|
132
|
-
|
|
133
|
-
|
|
83
|
+
// Events
|
|
84
|
+
peer.on('open', () => {})
|
|
85
|
+
peer.on('close', (reason) => {})
|
|
86
|
+
peer.on('message', (data) => {})
|
|
87
|
+
peer.on('error', (error) => {})
|
|
88
|
+
peer.on('reconnecting', (attempt, max) => {})
|
|
89
|
+
|
|
90
|
+
// Properties & Methods
|
|
91
|
+
peer.state // 'connecting' | 'connected' | 'reconnecting' | ...
|
|
92
|
+
peer.peerUsername
|
|
93
|
+
peer.send(data)
|
|
94
|
+
peer.close()
|
|
134
95
|
```
|
|
135
96
|
|
|
136
|
-
###
|
|
97
|
+
### rondevu.offer()
|
|
137
98
|
|
|
138
99
|
```typescript
|
|
139
|
-
|
|
140
|
-
|
|
100
|
+
const offer = await rondevu.offer({
|
|
101
|
+
tags: string[],
|
|
102
|
+
maxOffers: number,
|
|
103
|
+
ttl?: number, // Offer lifetime in ms (default: 300000)
|
|
104
|
+
autoStart?: boolean // Auto-start filling (default: true)
|
|
105
|
+
})
|
|
141
106
|
|
|
142
|
-
//
|
|
143
|
-
await rondevu.discoverService('chat:1.0.0')
|
|
107
|
+
offer.cancel() // Stop accepting connections
|
|
144
108
|
|
|
145
|
-
|
|
146
|
-
|
|
109
|
+
rondevu.on('connection:opened', (offerId, connection) => {
|
|
110
|
+
connection.on('message', (data) => {})
|
|
111
|
+
connection.send('Hello!')
|
|
112
|
+
})
|
|
147
113
|
```
|
|
148
114
|
|
|
149
|
-
###
|
|
115
|
+
### rondevu.discover()
|
|
150
116
|
|
|
151
117
|
```typescript
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
service?: string, // Service without username (for discovery)
|
|
155
|
-
username?: string, // Target username (combined with service)
|
|
156
|
-
onConnection?: (context) => void, // Called when data channel opens
|
|
157
|
-
rtcConfig?: RTCConfiguration // Optional: override ICE servers
|
|
158
|
-
})
|
|
118
|
+
const result = await rondevu.discover(['chat'], { limit: 20 })
|
|
119
|
+
result.offers.forEach(o => console.log(o.username, o.tags))
|
|
159
120
|
```
|
|
160
121
|
|
|
161
|
-
##
|
|
122
|
+
## Credentials
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// Auto-generated username
|
|
126
|
+
const rondevu = await Rondevu.connect()
|
|
127
|
+
// rondevu.getName() === 'friendly-panda-a1b2c3'
|
|
128
|
+
|
|
129
|
+
// Claimed username
|
|
130
|
+
const rondevu = await Rondevu.connect({ username: 'alice' })
|
|
131
|
+
|
|
132
|
+
// Save and restore credentials
|
|
133
|
+
const credential = rondevu.getCredential()
|
|
134
|
+
localStorage.setItem('cred', JSON.stringify(credential))
|
|
135
|
+
|
|
136
|
+
const saved = JSON.parse(localStorage.getItem('cred'))
|
|
137
|
+
const rondevu = await Rondevu.connect({ credential: saved })
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Tag Validation
|
|
141
|
+
|
|
142
|
+
Tags: 1-64 chars, lowercase alphanumeric with dots/dashes.
|
|
162
143
|
|
|
163
|
-
|
|
164
|
-
- Detailed API reference for all methods
|
|
165
|
-
- Type definitions and interfaces
|
|
166
|
-
- Platform support (Browser & Node.js)
|
|
167
|
-
- Advanced usage patterns
|
|
168
|
-
- Username rules and service FQN format
|
|
169
|
-
- Examples and migration guides
|
|
144
|
+
Valid: `chat`, `video-call`, `com.example.service`
|
|
170
145
|
|
|
171
|
-
##
|
|
146
|
+
## Links
|
|
172
147
|
|
|
173
|
-
- [
|
|
148
|
+
- [Live Demo](https://ronde.vu) | [Server](https://github.com/xtr-dev/rondevu-server) | [API](https://api.ronde.vu)
|
|
174
149
|
|
|
175
150
|
## License
|
|
176
151
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC Request Batcher with throttling
|
|
3
|
+
*
|
|
4
|
+
* Collects RPC requests over a short time window and sends them efficiently.
|
|
5
|
+
*
|
|
6
|
+
* Due to server authentication design (signature covers method+params),
|
|
7
|
+
* authenticated requests are sent individually while unauthenticated
|
|
8
|
+
* requests can be truly batched together.
|
|
9
|
+
*/
|
|
10
|
+
export interface RpcRequest {
|
|
11
|
+
method: string;
|
|
12
|
+
params?: any;
|
|
13
|
+
}
|
|
14
|
+
export interface RpcResponse {
|
|
15
|
+
success: boolean;
|
|
16
|
+
result?: any;
|
|
17
|
+
error?: string;
|
|
18
|
+
errorCode?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface BatcherOptions {
|
|
21
|
+
/** Delay in ms before flushing queued requests (default: 10) */
|
|
22
|
+
delay?: number;
|
|
23
|
+
/** Maximum batch size for unauthenticated requests (default: 50) */
|
|
24
|
+
maxBatchSize?: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* RpcBatcher - Batches RPC requests with throttling
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* const batcher = new RpcBatcher('https://api.example.com', {
|
|
32
|
+
* delay: 10,
|
|
33
|
+
* maxBatchSize: 50
|
|
34
|
+
* })
|
|
35
|
+
*
|
|
36
|
+
* // Requests made within the delay window are batched
|
|
37
|
+
* const [result1, result2] = await Promise.all([
|
|
38
|
+
* batcher.add({ method: 'getOffer', params: {...} }, null),
|
|
39
|
+
* batcher.add({ method: 'getOffer', params: {...} }, null)
|
|
40
|
+
* ])
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare class RpcBatcher {
|
|
44
|
+
private readonly baseUrl;
|
|
45
|
+
private queue;
|
|
46
|
+
private flushTimer;
|
|
47
|
+
private readonly delay;
|
|
48
|
+
private readonly maxBatchSize;
|
|
49
|
+
constructor(baseUrl: string, options?: BatcherOptions);
|
|
50
|
+
/**
|
|
51
|
+
* Add a request to the batch queue
|
|
52
|
+
* @param request - The RPC request
|
|
53
|
+
* @param authHeaders - Auth headers for authenticated requests, null for unauthenticated
|
|
54
|
+
* @returns Promise that resolves with the request result
|
|
55
|
+
*/
|
|
56
|
+
add(request: RpcRequest, authHeaders: Record<string, string> | null): Promise<any>;
|
|
57
|
+
/**
|
|
58
|
+
* Schedule a flush after the delay
|
|
59
|
+
*/
|
|
60
|
+
private scheduleFlush;
|
|
61
|
+
/**
|
|
62
|
+
* Flush all queued requests
|
|
63
|
+
*/
|
|
64
|
+
private flush;
|
|
65
|
+
/**
|
|
66
|
+
* Process unauthenticated requests in batches
|
|
67
|
+
*/
|
|
68
|
+
private processUnauthenticatedBatches;
|
|
69
|
+
/**
|
|
70
|
+
* Process authenticated requests individually
|
|
71
|
+
* Each authenticated request needs its own HTTP call because
|
|
72
|
+
* the signature covers the specific method+params
|
|
73
|
+
*/
|
|
74
|
+
private processAuthenticatedRequests;
|
|
75
|
+
/**
|
|
76
|
+
* Send a batch of requests
|
|
77
|
+
*/
|
|
78
|
+
private sendBatch;
|
|
79
|
+
/**
|
|
80
|
+
* Flush immediately (useful for cleanup/testing)
|
|
81
|
+
*/
|
|
82
|
+
flushNow(): Promise<void>;
|
|
83
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC Request Batcher with throttling
|
|
3
|
+
*
|
|
4
|
+
* Collects RPC requests over a short time window and sends them efficiently.
|
|
5
|
+
*
|
|
6
|
+
* Due to server authentication design (signature covers method+params),
|
|
7
|
+
* authenticated requests are sent individually while unauthenticated
|
|
8
|
+
* requests can be truly batched together.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* RpcBatcher - Batches RPC requests with throttling
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const batcher = new RpcBatcher('https://api.example.com', {
|
|
16
|
+
* delay: 10,
|
|
17
|
+
* maxBatchSize: 50
|
|
18
|
+
* })
|
|
19
|
+
*
|
|
20
|
+
* // Requests made within the delay window are batched
|
|
21
|
+
* const [result1, result2] = await Promise.all([
|
|
22
|
+
* batcher.add({ method: 'getOffer', params: {...} }, null),
|
|
23
|
+
* batcher.add({ method: 'getOffer', params: {...} }, null)
|
|
24
|
+
* ])
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export class RpcBatcher {
|
|
28
|
+
constructor(baseUrl, options = {}) {
|
|
29
|
+
this.baseUrl = baseUrl;
|
|
30
|
+
this.queue = [];
|
|
31
|
+
this.flushTimer = null;
|
|
32
|
+
this.delay = options.delay ?? 10;
|
|
33
|
+
this.maxBatchSize = options.maxBatchSize ?? 50;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Add a request to the batch queue
|
|
37
|
+
* @param request - The RPC request
|
|
38
|
+
* @param authHeaders - Auth headers for authenticated requests, null for unauthenticated
|
|
39
|
+
* @returns Promise that resolves with the request result
|
|
40
|
+
*/
|
|
41
|
+
add(request, authHeaders) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
this.queue.push({ request, authHeaders, resolve, reject });
|
|
44
|
+
this.scheduleFlush();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Schedule a flush after the delay
|
|
49
|
+
*/
|
|
50
|
+
scheduleFlush() {
|
|
51
|
+
if (this.flushTimer)
|
|
52
|
+
return;
|
|
53
|
+
this.flushTimer = setTimeout(() => {
|
|
54
|
+
this.flushTimer = null;
|
|
55
|
+
this.flush();
|
|
56
|
+
}, this.delay);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Flush all queued requests
|
|
60
|
+
*/
|
|
61
|
+
async flush() {
|
|
62
|
+
if (this.queue.length === 0)
|
|
63
|
+
return;
|
|
64
|
+
const items = this.queue;
|
|
65
|
+
this.queue = [];
|
|
66
|
+
// Separate authenticated vs unauthenticated requests
|
|
67
|
+
const unauthenticated = [];
|
|
68
|
+
const authenticated = [];
|
|
69
|
+
for (const item of items) {
|
|
70
|
+
if (item.authHeaders) {
|
|
71
|
+
authenticated.push(item);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
unauthenticated.push(item);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Process unauthenticated requests in batches
|
|
78
|
+
await this.processUnauthenticatedBatches(unauthenticated);
|
|
79
|
+
// Process authenticated requests individually (each needs unique signature)
|
|
80
|
+
await this.processAuthenticatedRequests(authenticated);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Process unauthenticated requests in batches
|
|
84
|
+
*/
|
|
85
|
+
async processUnauthenticatedBatches(items) {
|
|
86
|
+
if (items.length === 0)
|
|
87
|
+
return;
|
|
88
|
+
// Split into chunks of maxBatchSize
|
|
89
|
+
for (let i = 0; i < items.length; i += this.maxBatchSize) {
|
|
90
|
+
const chunk = items.slice(i, i + this.maxBatchSize);
|
|
91
|
+
await this.sendBatch(chunk, null);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Process authenticated requests individually
|
|
96
|
+
* Each authenticated request needs its own HTTP call because
|
|
97
|
+
* the signature covers the specific method+params
|
|
98
|
+
*/
|
|
99
|
+
async processAuthenticatedRequests(items) {
|
|
100
|
+
// Send all authenticated requests in parallel, each as its own batch of 1
|
|
101
|
+
await Promise.all(items.map(item => this.sendBatch([item], item.authHeaders)));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Send a batch of requests
|
|
105
|
+
*/
|
|
106
|
+
async sendBatch(items, authHeaders) {
|
|
107
|
+
try {
|
|
108
|
+
const requests = items.map(item => item.request);
|
|
109
|
+
const headers = {
|
|
110
|
+
'Content-Type': 'application/json',
|
|
111
|
+
};
|
|
112
|
+
if (authHeaders) {
|
|
113
|
+
Object.assign(headers, authHeaders);
|
|
114
|
+
}
|
|
115
|
+
const response = await fetch(`${this.baseUrl}/rpc`, {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers,
|
|
118
|
+
body: JSON.stringify(requests), // Always send as array
|
|
119
|
+
});
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
122
|
+
items.forEach(item => item.reject(error));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const results = await response.json();
|
|
126
|
+
// Match responses to requests (server returns array in same order)
|
|
127
|
+
items.forEach((item, index) => {
|
|
128
|
+
const result = results[index];
|
|
129
|
+
if (!result) {
|
|
130
|
+
item.reject(new Error('Missing response from server'));
|
|
131
|
+
}
|
|
132
|
+
else if (!result.success) {
|
|
133
|
+
item.reject(new Error(result.error || 'RPC call failed'));
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
item.resolve(result.result);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
// Network or parsing error - reject all
|
|
142
|
+
items.forEach(item => item.reject(error));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Flush immediately (useful for cleanup/testing)
|
|
147
|
+
*/
|
|
148
|
+
async flushNow() {
|
|
149
|
+
if (this.flushTimer) {
|
|
150
|
+
clearTimeout(this.flushTimer);
|
|
151
|
+
this.flushTimer = null;
|
|
152
|
+
}
|
|
153
|
+
await this.flush();
|
|
154
|
+
}
|
|
155
|
+
}
|