@@ -9,10 +9,12 @@ use spacetimedb_data_structures::map::HashMap;
99use spacetimedb_lib:: db:: raw_def:: v9:: RawModuleDefV9 ;
1010use spacetimedb_lib:: de:: serde:: { DeserializeWrapper , SeedWrapper } ;
1111use spacetimedb_lib:: ser:: serde:: SerializeWrapper ;
12+ use std:: io;
1213use std:: time:: Duration ;
14+ use thiserror:: Error ;
1315use tokio:: io:: AsyncWriteExt ;
1416use tokio_tungstenite:: tungstenite:: client:: IntoClientRequest ;
15- use tokio_tungstenite:: tungstenite:: Message as WsMessage ;
17+ use tokio_tungstenite:: tungstenite:: { Error as WsError , Message as WsMessage } ;
1618
1719use crate :: api:: ClientApi ;
1820use crate :: common_args;
@@ -155,35 +157,88 @@ pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error
155157 if let Some ( auth_header) = api. con . auth_header . to_header ( ) {
156158 req. headers_mut ( ) . insert ( header:: AUTHORIZATION , auth_header) ;
157159 }
158- let ( mut ws, _ ) = tokio_tungstenite:: connect_async ( req) . await ?;
160+ let mut ws = tokio_tungstenite:: connect_async ( req) . await . map ( | ( ws , _ ) | ws ) ?;
159161
160162 let task = async {
161163 subscribe ( & mut ws, queries. cloned ( ) . map ( Into :: into) . collect ( ) ) . await ?;
162164 await_initial_update ( & mut ws, print_initial_update. then_some ( & module_def) ) . await ?;
163165 consume_transaction_updates ( & mut ws, num, & module_def) . await
164166 } ;
165167
166- let needs_shutdown = if let Some ( timeout) = timeout {
168+ let res = if let Some ( timeout) = timeout {
167169 let timeout = Duration :: from_secs ( timeout. into ( ) ) ;
168170 match tokio:: time:: timeout ( timeout, task) . await {
169- Ok ( res) => res?,
170- Err ( _elapsed) => true ,
171+ Ok ( res) => res,
172+ Err ( _elapsed) => {
173+ eprintln ! ( "timed out after {}s" , timeout. as_secs( ) ) ;
174+ Ok ( ( ) )
175+ }
171176 }
172177 } else {
173- task. await ?
178+ task. await
174179 } ;
175180
176- if needs_shutdown {
177- ws. close ( None ) . await ?;
178- }
181+ // Close the connection gracefully.
182+ // This will return an error if the server already closed,
183+ // or the connection is in a bad state.
184+ // The error (if any) relevant to the user is already stored in `res`,
185+ // so we can ignore errors here -- graceful close is basically a
186+ // courtesy to the server.
187+ let _ = ws. close ( None ) . await ;
188+ // The server closing the connection is not considered an error,
189+ // but any other error is.
190+ res. or_else ( |e| {
191+ if e. is_server_closed_connection ( ) {
192+ Ok ( ( ) )
193+ } else {
194+ Err ( e)
195+ }
196+ } )
197+ . map_err ( anyhow:: Error :: from)
198+ }
179199
180- Ok ( ( ) )
200+ #[ derive( Debug , Error ) ]
201+ enum Error {
202+ #[ error( "error sending subscription queries" ) ]
203+ Subscribe {
204+ #[ source]
205+ source : WsError ,
206+ } ,
207+ #[ error( "protocol error: {details}" ) ]
208+ Protocol { details : & ' static str } ,
209+ #[ error( "websocket error: {source}" ) ]
210+ Websocket {
211+ #[ source]
212+ source : WsError ,
213+ } ,
214+ #[ error( "encountered failed transaction: {reason}" ) ]
215+ TransactionFailure { reason : Box < str > } ,
216+ #[ error( "error formatting response: {source:#}" ) ]
217+ Reformat {
218+ #[ source]
219+ source : anyhow:: Error ,
220+ } ,
221+ #[ error( transparent) ]
222+ Serde ( #[ from] serde_json:: Error ) ,
223+ #[ error( transparent) ]
224+ Io ( #[ from] io:: Error ) ,
225+ }
226+
227+ impl Error {
228+ fn is_server_closed_connection ( & self ) -> bool {
229+ matches ! (
230+ self ,
231+ Self :: Websocket {
232+ source: WsError :: ConnectionClosed
233+ }
234+ )
235+ }
181236}
182237
183238/// Send the subscribe message.
184- async fn subscribe < S > ( ws : & mut S , query_strings : Box < [ Box < str > ] > ) -> Result < ( ) , S :: Error >
239+ async fn subscribe < S > ( ws : & mut S , query_strings : Box < [ Box < str > ] > ) -> Result < ( ) , Error >
185240where
186- S : Sink < WsMessage > + Unpin ,
241+ S : Sink < WsMessage , Error = WsError > + Unpin ,
187242{
188243 let msg = serde_json:: to_string ( & SerializeWrapper :: new ( ws:: ClientMessage :: < ( ) > :: Subscribe (
189244 ws:: Subscribe {
@@ -192,35 +247,39 @@ where
192247 } ,
193248 ) ) )
194249 . unwrap ( ) ;
195- ws. send ( msg. into ( ) ) . await
250+ ws. send ( msg. into ( ) ) . await . map_err ( |source| Error :: Subscribe { source } )
196251}
197252
198253/// Await the initial [`ServerMessage::SubscriptionUpdate`].
199254/// If `module_def` is `Some`, print a JSON representation to stdout.
200- async fn await_initial_update < S > ( ws : & mut S , module_def : Option < & RawModuleDefV9 > ) -> anyhow :: Result < ( ) >
255+ async fn await_initial_update < S > ( ws : & mut S , module_def : Option < & RawModuleDefV9 > ) -> Result < ( ) , Error >
201256where
202- S : TryStream < Ok = WsMessage > + Unpin ,
203- S :: Error : std:: error:: Error + Send + Sync + ' static ,
257+ S : TryStream < Ok = WsMessage , Error = WsError > + Unpin ,
204258{
205259 const RECV_TX_UPDATE : & str = "protocol error: received transaction update before initial subscription update" ;
206260
207- while let Some ( msg) = ws. try_next ( ) . await ? {
261+ while let Some ( msg) = ws. try_next ( ) . await . map_err ( |source| Error :: Websocket { source } ) ? {
208262 let Some ( msg) = parse_msg_json ( & msg) else { continue } ;
209263 match msg {
210264 ws:: ServerMessage :: InitialSubscription ( sub) => {
211265 if let Some ( module_def) = module_def {
212- let formatted = reformat_update ( & sub. database_update , module_def) ?;
213- let output = serde_json:: to_string ( & formatted) ? + "\n " ;
266+ let output = format_output_json ( & sub. database_update , module_def) ?;
214267 tokio:: io:: stdout ( ) . write_all ( output. as_bytes ( ) ) . await ?
215268 }
216269 break ;
217270 }
218- ws:: ServerMessage :: TransactionUpdate ( ws:: TransactionUpdate { status, .. } ) => anyhow:: bail!( match status {
219- ws:: UpdateStatus :: Failed ( msg) => msg,
220- _ => RECV_TX_UPDATE . into( ) ,
221- } ) ,
271+ ws:: ServerMessage :: TransactionUpdate ( ws:: TransactionUpdate { status, .. } ) => {
272+ return Err ( match status {
273+ ws:: UpdateStatus :: Failed ( msg) => Error :: TransactionFailure { reason : msg } ,
274+ _ => Error :: Protocol {
275+ details : RECV_TX_UPDATE ,
276+ } ,
277+ } )
278+ }
222279 ws:: ServerMessage :: TransactionUpdateLight ( ws:: TransactionUpdateLight { .. } ) => {
223- anyhow:: bail!( RECV_TX_UPDATE )
280+ return Err ( Error :: Protocol {
281+ details : RECV_TX_UPDATE ,
282+ } )
224283 }
225284 _ => continue ,
226285 }
@@ -231,41 +290,47 @@ where
231290
232291/// Print `num` [`ServerMessage::TransactionUpdate`] messages as JSON.
233292/// If `num` is `None`, keep going indefinitely.
234- async fn consume_transaction_updates < S > (
235- ws : & mut S ,
236- num : Option < u32 > ,
237- module_def : & RawModuleDefV9 ,
238- ) -> anyhow:: Result < bool >
293+ async fn consume_transaction_updates < S > ( ws : & mut S , num : Option < u32 > , module_def : & RawModuleDefV9 ) -> Result < ( ) , Error >
239294where
240- S : TryStream < Ok = WsMessage > + Unpin ,
241- S :: Error : std:: error:: Error + Send + Sync + ' static ,
295+ S : TryStream < Ok = WsMessage , Error = WsError > + Unpin ,
242296{
243297 let mut stdout = tokio:: io:: stdout ( ) ;
244298 let mut num_received = 0 ;
245299 loop {
246300 if num. is_some_and ( |n| num_received >= n) {
247- break Ok ( true ) ;
301+ return Ok ( ( ) ) ;
248302 }
249- let Some ( msg) = ws. try_next ( ) . await ? else {
303+ let Some ( msg) = ws. try_next ( ) . await . map_err ( |source| Error :: Websocket { source } ) ? else {
250304 eprintln ! ( "disconnected by server" ) ;
251- break Ok ( false ) ;
305+ return Err ( Error :: Websocket {
306+ source : WsError :: ConnectionClosed ,
307+ } ) ;
252308 } ;
253309
254310 let Some ( msg) = parse_msg_json ( & msg) else { continue } ;
255311 match msg {
256312 ws:: ServerMessage :: InitialSubscription ( _) => {
257- anyhow:: bail!( "protocol error: received a second initial subscription update" )
313+ return Err ( Error :: Protocol {
314+ details : "received a second initial subscription update" ,
315+ } )
258316 }
259317 ws:: ServerMessage :: TransactionUpdateLight ( ws:: TransactionUpdateLight { update, .. } )
260318 | ws:: ServerMessage :: TransactionUpdate ( ws:: TransactionUpdate {
261319 status : ws:: UpdateStatus :: Committed ( update) ,
262320 ..
263321 } ) => {
264- let output = serde_json :: to_string ( & reformat_update ( & update, module_def) ?) ? + " \n " ;
322+ let output = format_output_json ( & update, module_def) ?;
265323 stdout. write_all ( output. as_bytes ( ) ) . await ?;
266324 num_received += 1 ;
267325 }
268326 _ => continue ,
269327 }
270328 }
271329}
330+
331+ fn format_output_json ( msg : & ws:: DatabaseUpdate < JsonFormat > , schema : & RawModuleDefV9 ) -> Result < String , Error > {
332+ let formatted = reformat_update ( msg, schema) . map_err ( |source| Error :: Reformat { source } ) ?;
333+ let output = serde_json:: to_string ( & formatted) ? + "\n " ;
334+
335+ Ok ( output)
336+ }
0 commit comments