@@ -95,6 +95,7 @@ export class ActorConnRaw {
95
95
private readonly supportedTransports : Transport [ ] ,
96
96
private readonly serverTransports : Transport [ ] ,
97
97
private readonly dynamicImports : DynamicImports ,
98
+ private readonly actorQuery : unknown ,
98
99
) {
99
100
this . #keepNodeAliveInterval = setInterval ( ( ) => 60_000 ) ;
100
101
}
@@ -115,34 +116,135 @@ export class ActorConnRaw {
115
116
) : Promise < Response > {
116
117
logger ( ) . debug ( "action" , { name, args } ) ;
117
118
118
- // TODO: Add to queue if socket is not open
119
+ // Check if we have an active websocket connection
120
+ if ( this . #transport) {
121
+ // If we have an active connection, use the websocket RPC
122
+ const rpcId = this . #rpcIdCounter;
123
+ this . #rpcIdCounter += 1 ;
119
124
120
- const rpcId = this . #rpcIdCounter;
121
- this . #rpcIdCounter += 1 ;
125
+ const { promise, resolve, reject } =
126
+ Promise . withResolvers < wsToClient . RpcResponseOk > ( ) ;
127
+ this . #rpcInFlight. set ( rpcId , { name, resolve, reject } ) ;
122
128
123
- const { promise, resolve, reject } =
124
- Promise . withResolvers < wsToClient . RpcResponseOk > ( ) ;
125
- this . #rpcInFlight. set ( rpcId , { name, resolve, reject } ) ;
126
-
127
- this . #sendMessage( {
128
- b : {
129
- rr : {
130
- i : rpcId ,
131
- n : name ,
132
- a : args ,
129
+ this . #sendMessage( {
130
+ b : {
131
+ rr : {
132
+ i : rpcId ,
133
+ n : name ,
134
+ a : args ,
135
+ } ,
133
136
} ,
134
- } ,
135
- } satisfies wsToServer . ToServer ) ;
137
+ } satisfies wsToServer . ToServer ) ;
136
138
137
- // TODO: Throw error if disconnect is called
139
+ // TODO: Throw error if disconnect is called
138
140
139
- const { i : responseId , o : output } = await promise ;
140
- if ( responseId !== rpcId )
141
- throw new Error (
142
- `Request ID ${ rpcId } does not match response ID ${ responseId } ` ,
143
- ) ;
141
+ const { i : responseId , o : output } = await promise ;
142
+ if ( responseId !== rpcId )
143
+ throw new Error (
144
+ `Request ID ${ rpcId } does not match response ID ${ responseId } ` ,
145
+ ) ;
146
+
147
+ return output as Response ;
148
+ } else {
149
+ // If no websocket connection, use HTTP RPC via manager
150
+ try {
151
+ // Get the manager endpoint from the endpoint provided
152
+ const managerEndpoint = this . endpoint . split ( '/manager/' ) [ 0 ] ;
153
+ const actorQueryStr = encodeURIComponent ( JSON . stringify ( this . actorQuery ) ) ;
154
+
155
+ const url = `${ managerEndpoint } /actor/rpc/${ name } ?query=${ actorQueryStr } ` ;
156
+ logger ( ) . debug ( "=== CLIENT HTTP RPC: Sending request ===" , {
157
+ url,
158
+ managerEndpoint,
159
+ actorQuery : this . actorQuery ,
160
+ name,
161
+ args
162
+ } ) ;
163
+
164
+ try {
165
+ const response = await fetch ( url , {
166
+ method : "POST" ,
167
+ headers : {
168
+ "Content-Type" : "application/json" ,
169
+ } ,
170
+ body : JSON . stringify ( {
171
+ a : args ,
172
+ } ) ,
173
+ } ) ;
144
174
145
- return output as Response ;
175
+ logger ( ) . debug ( "=== CLIENT HTTP RPC: Response received ===" , {
176
+ status : response . status ,
177
+ ok : response . ok ,
178
+ headers : Object . fromEntries ( [ ...response . headers ] )
179
+ } ) ;
180
+
181
+ if ( ! response . ok ) {
182
+ try {
183
+ const errorData = await response . json ( ) ;
184
+ logger ( ) . error ( "=== CLIENT HTTP RPC: Error response ===" , { errorData } ) ;
185
+ throw new errors . ActionError (
186
+ errorData . c || "RPC_ERROR" ,
187
+ errorData . m || "RPC call failed" ,
188
+ errorData . md ,
189
+ ) ;
190
+ } catch ( parseError ) {
191
+ // If response is not JSON, get it as text and throw generic error
192
+ const errorText = await response . text ( ) ;
193
+ logger ( ) . error ( "=== CLIENT HTTP RPC: Error parsing response ===" , {
194
+ errorText,
195
+ parseError
196
+ } ) ;
197
+ throw new errors . ActionError (
198
+ "RPC_ERROR" ,
199
+ `RPC call failed: ${ errorText } ` ,
200
+ { } ,
201
+ ) ;
202
+ }
203
+ }
204
+
205
+ // Clone response to avoid consuming it
206
+ const responseClone = response . clone ( ) ;
207
+ const responseText = await responseClone . text ( ) ;
208
+ logger ( ) . debug ( "=== CLIENT HTTP RPC: Response body ===" , { responseText } ) ;
209
+
210
+ // Parse response body
211
+ try {
212
+ const responseData = JSON . parse ( responseText ) ;
213
+ logger ( ) . debug ( "=== CLIENT HTTP RPC: Parsed response ===" , { responseData } ) ;
214
+ return responseData . o as Response ;
215
+ } catch ( parseError ) {
216
+ logger ( ) . error ( "=== CLIENT HTTP RPC: Error parsing JSON ===" , {
217
+ responseText,
218
+ parseError
219
+ } ) ;
220
+ throw new errors . ActionError (
221
+ "RPC_ERROR" ,
222
+ `Failed to parse response: ${ parseError } ` ,
223
+ { responseText }
224
+ ) ;
225
+ }
226
+ } catch ( fetchError ) {
227
+ logger ( ) . error ( "=== CLIENT HTTP RPC: Fetch error ===" , {
228
+ error : fetchError ,
229
+ url
230
+ } ) ;
231
+ throw new errors . ActionError (
232
+ "RPC_ERROR" ,
233
+ `Fetch failed: ${ fetchError } ` ,
234
+ { cause : fetchError }
235
+ ) ;
236
+ }
237
+ } catch ( error ) {
238
+ if ( error instanceof errors . ActionError ) {
239
+ throw error ;
240
+ }
241
+ throw new errors . ActionError (
242
+ "RPC_ERROR" ,
243
+ `Failed to execute RPC ${ name } : ${ error } ` ,
244
+ { cause : error }
245
+ ) ;
246
+ }
247
+ }
146
248
}
147
249
148
250
//async #rpcHttp<Args extends Array<unknown> = unknown[], Response = unknown>(name: string, ...args: Args): Promise<Response> {
453
555
}
454
556
455
557
#buildConnUrl( transport : Transport ) : string {
456
- let url = `${ this . endpoint } /connect/${ transport } ?encoding=${ this . encodingKind } ` ;
558
+ // Get the manager endpoint from the endpoint provided
559
+ const managerEndpoint = this . endpoint . split ( '/manager/' ) [ 0 ] ;
560
+ const actorQueryStr = encodeURIComponent ( JSON . stringify ( this . actorQuery ) ) ;
561
+
562
+ logger ( ) . debug ( "=== Client building conn URL ===" , {
563
+ originalEndpoint : this . endpoint ,
564
+ managerEndpoint : managerEndpoint ,
565
+ transport : transport
566
+ } ) ;
567
+
568
+ let url = `${ managerEndpoint } /actor/connect/${ transport } ?encoding=${ this . encodingKind } &query=${ actorQueryStr } ` ;
457
569
458
570
if ( this . params !== undefined ) {
459
571
const paramsStr = JSON . stringify ( this . params ) ;
469
581
if ( transport === "websocket" ) {
470
582
url = url . replace ( / ^ h t t p : / , "ws:" ) . replace ( / ^ h t t p s : / , "wss:" ) ;
471
583
}
584
+
585
+ logger ( ) . debug ( "=== Client final conn URL ===" , { url } ) ;
472
586
473
587
return url ;
474
588
}
617
731
if ( ! this . #connectionId || ! this . #connectionToken)
618
732
throw new errors . InternalError ( "Missing connection ID or token." ) ;
619
733
620
- let url = `${ this . endpoint } /connections/${ this . #connectionId} /message?encoding=${ this . encodingKind } &connectionToken=${ encodeURIComponent ( this . #connectionToken) } ` ;
734
+ // Get the manager endpoint from the endpoint provided
735
+ const managerEndpoint = this . endpoint . split ( '/manager/' ) [ 0 ] ;
736
+ const actorQueryStr = encodeURIComponent ( JSON . stringify ( this . actorQuery ) ) ;
737
+
738
+ let url = `${ managerEndpoint } /actor/connections/${ this . #connectionId} /message?encoding=${ this . encodingKind } &connectionToken=${ encodeURIComponent ( this . #connectionToken) } &query=${ actorQueryStr } ` ;
621
739
622
740
// TODO: Implement ordered messages, this is not guaranteed order. Needs to use an index in order to ensure we can pipeline requests efficiently.
623
741
// TODO: Validate that we're using HTTP/3 whenever possible for pipelining requests
0 commit comments