docstr: Send a message to a client.
  
  If the client is likely to have already cached the message, we may
  instead send a "reference" message that contains only the hash of the
  message.
  
  Parameters
  ----------
  session_info : ActiveSessionInfo
      The ActiveSessionInfo associated with websocket
  msg : ForwardMsg
      The message to send to the client
  
  Notes
  -----
  Threading: UNSAFE. Must be called on the eventloop thread.
code: def _send_message(
      self, session_info: ActiveSessionInfo, msg: ForwardMsg
  ) -> None:
      """Send a message to a client.
  
      If the client is likely to have already cached the message, we may
      instead send a "reference" message that contains only the hash of the
      message.
  
      Parameters
      ----------
      session_info : ActiveSessionInfo
          The ActiveSessionInfo associated with websocket
      msg : ForwardMsg
          The message to send to the client
  
      Notes
      -----
      Threading: UNSAFE. Must be called on the eventloop thread.
      """
      msg.metadata.cacheable = is_cacheable_msg(msg)
      msg_to_send = msg
      if msg.metadata.cacheable:
          populate_hash_if_needed(msg)
  
          if self._message_cache.has_message_reference(
              msg, session_info.session, session_info.script_run_count
          ):
              # This session has probably cached this message. Send
              # a reference instead.
              LOGGER.debug(
                  "Sending cached message ref (hash=%s)", msg.hash
              )
              msg_to_send = create_reference_msg(msg)
  
          # Cache the message so it can be referenced in the future.
          # If the message is already cached, this will reset its
          # age.
          LOGGER.debug("Caching message (hash=%s)", msg.hash)
          self._message_cache.add_message(
              msg, session_info.session, session_info.script_run_count
          )
  
      # If this was a `script_finished` message, we increment the
      # script_run_count for this session, and update the cache
      if (
          msg.WhichOneof("type") == "script_finished"
          and msg.script_finished == ForwardMsg.FINISHED_SUCCESSFULLY
      ):
          LOGGER.debug(
              "Script run finished successfully; "
              "removing expired entries from MessageCache "
              "(max_age=%s)",
              config.get_option("global.maxCachedMessageAge"),
          )
          session_info.script_run_count += 1
          self._message_cache.remove_expired_entries_for_session(
              session_info.session, session_info.script_run_count
          )
  
      # Ship it off!
      session_info.client.write_forward_msg(msg_to_send)
  
imports: from __future__ import annotations
  import asyncio
  import time
  import traceback
  from dataclasses import dataclass, field
  from enum import Enum
  from typing import TYPE_CHECKING, Awaitable, Dict, NamedTuple, Optional, Tuple, Type
  from typing_extensions import Final
  from streamlit import config
  from streamlit.logger import get_logger
  from streamlit.proto.BackMsg_pb2 import BackMsg
  from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
  from streamlit.runtime.app_session import AppSession
  from streamlit.runtime.caching import get_data_cache_stats_provider, get_resource_cache_stats_provider
  from streamlit.runtime.caching.storage.local_disk_cache_storage import LocalDiskCacheStorageManager
  from streamlit.runtime.forward_msg_cache import ForwardMsgCache, create_reference_msg, populate_hash_if_needed
  from streamlit.runtime.legacy_caching.caching import _mem_caches
  from streamlit.runtime.media_file_manager import MediaFileManager
  from streamlit.runtime.media_file_storage import MediaFileStorage
  from streamlit.runtime.memory_session_storage import MemorySessionStorage
  from streamlit.runtime.runtime_util import is_cacheable_msg
  from streamlit.runtime.script_data import ScriptData
  from streamlit.runtime.scriptrunner.script_cache import ScriptCache
  from streamlit.runtime.session_manager import ActiveSessionInfo, SessionClient, SessionClientDisconnectedError, SessionManager, SessionStorage
  from streamlit.runtime.state import SCRIPT_RUN_WITHOUT_ERRORS_KEY, SessionStateStatProvider
  from streamlit.runtime.stats import StatsManager
  from streamlit.runtime.uploaded_file_manager import UploadedFileManager
  from streamlit.runtime.websocket_session_manager import WebsocketSessionManager
  from streamlit.watcher import LocalSourcesWatcher
label_desc: Use image node_id 21 for calling the Runtime obj's underlying member method code with example usage: obj._send_message(session_info, msg) without return types