@@ -303,11 +303,11 @@ async def handle_client(self) -> web.WebSocketResponse:
303303 self ._writer_task = self .mass .create_task (self ._writer ())
304304
305305 # send server(version) info when client connects
306- self ._send_message (self .mass .get_server_info ())
306+ await self ._send_message (self .mass .get_server_info ())
307307
308308 # forward all events to clients
309309 def handle_event (event : MassEvent ) -> None :
310- self ._send_message (event )
310+ self ._send_message_sync (event )
311311
312312 unsub_callback = self .mass .subscribe (handle_event )
313313
@@ -331,7 +331,7 @@ def handle_event(event: MassEvent) -> None:
331331 disconnect_warn = f"Received invalid JSON: { msg .data } "
332332 break
333333
334- self ._handle_command (command_msg )
334+ await self ._handle_command (command_msg )
335335
336336 except asyncio .CancelledError :
337337 self ._logger .debug ("Connection closed by client" )
@@ -360,15 +360,15 @@ def handle_event(event: MassEvent) -> None:
360360
361361 return wsock
362362
363- def _handle_command (self , msg : CommandMessage ) -> None :
363+ async def _handle_command (self , msg : CommandMessage ) -> None :
364364 """Handle an incoming command from the client."""
365365 self ._logger .debug ("Handling command %s" , msg .command )
366366
367367 # work out handler for the given path/command
368368 handler = self .mass .command_handlers .get (msg .command )
369369
370370 if handler is None :
371- self ._send_message (
371+ await self ._send_message (
372372 ErrorResultMessage (
373373 msg .message_id ,
374374 InvalidCommand .error_code ,
@@ -392,20 +392,20 @@ async def _run_handler(self, handler: APICommandHandler, msg: CommandMessage) ->
392392 async for item in iterator :
393393 result .append (item )
394394 if len (result ) >= 500 :
395- self ._send_message (
395+ await self ._send_message (
396396 SuccessResultMessage (msg .message_id , result , partial = True )
397397 )
398398 result = []
399399 elif asyncio .iscoroutine (result ):
400400 result = await result
401- self ._send_message (SuccessResultMessage (msg .message_id , result ))
401+ await self ._send_message (SuccessResultMessage (msg .message_id , result ))
402402 except Exception as err :
403403 if self ._logger .isEnabledFor (logging .DEBUG ):
404404 self ._logger .exception ("Error handling message: %s" , msg )
405405 else :
406406 self ._logger .error ("Error handling message: %s: %s" , msg .command , str (err ))
407407 err_msg = str (err ) or err .__class__ .__name__
408- self ._send_message (
408+ await self ._send_message (
409409 ErrorResultMessage (msg .message_id , getattr (err , "error_code" , 999 ), err_msg )
410410 )
411411
@@ -424,13 +424,30 @@ async def _writer(self) -> None:
424424 self ._logger .log (VERBOSE_LOG_LEVEL , "Writing: %s" , message )
425425 await self .wsock .send_str (message )
426426
427- def _send_message (self , message : MessageType ) -> None :
428- """Send a message to the client.
427+ async def _send_message (self , message : MessageType ) -> None :
428+ """Send a message to the client (for large response messages) .
429429
430+ Runs JSON serialization in executor to avoid blocking for large messages.
430431 Closes connection if the client is not reading the messages.
431432
432433 Async friendly.
433434 """
435+ # Run JSON serialization in executor to avoid blocking for large messages
436+ loop = asyncio .get_running_loop ()
437+ _message = await loop .run_in_executor (None , message .to_json )
438+
439+ try :
440+ self ._to_write .put_nowait (_message )
441+ except asyncio .QueueFull :
442+ self ._logger .error ("Client exceeded max pending messages: %s" , MAX_PENDING_MSG )
443+
444+ self ._cancel ()
445+
446+ def _send_message_sync (self , message : MessageType ) -> None :
447+ """Send a message from a sync context (for small messages like events).
448+
449+ Serializes inline without executor overhead since events are typically small.
450+ """
434451 _message = message .to_json ()
435452
436453 try :
0 commit comments