packages feed

synapse-cc-0.3.2: src/SynapseCC/Wait.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | Wait command: block until configured backends are reachable.
--
-- Usage:
--   synapse-cc wait                         -- wait for all backends in config
--   synapse-cc wait monochrome              -- wait for one backend (URL from config)
--   synapse-cc wait monochrome -u ws://...: -- wait for explicit backend+URL
module SynapseCC.Wait
  ( runWait
  ) where

import Control.Concurrent (threadDelay)
import Control.Exception (try, SomeException)
import qualified Data.Map.Strict as Map
import Data.Maybe (fromMaybe)
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time.Clock (getCurrentTime, diffUTCTime)
import Control.Monad (when)

import qualified Data.Aeson as Aeson
import Plexus.Client (SubstrateConfig(..), defaultConfig)
import qualified Plexus.Transport as Transport

import SynapseCC.Config (loadSynapseConfig, parseWsUrl)
import SynapseCC.Logging
import SynapseCC.Types

-- | Extract all unique (backend, url) pairs from the config.
collectEndpoints :: SynapseConfig -> [(Text, Text)]
collectEndpoints sc =
  let defaultUrl = fromMaybe "ws://127.0.0.1:4444" (scUrl sc)
      defaultBk  = fromMaybe "substrate" (scBackend sc)
      pairs = [ let bk  = fromMaybe defaultBk (tcBackend tc)
                    url = fromMaybe defaultUrl (tcUrl tc)
                in (bk, url)
              | tc <- Map.elems (scTargets sc)
              ]
  in Map.toList (Map.fromList pairs)

-- | Try to connect to a backend by calling {backend}.hash
probeBackend :: Text -> Text -> IO (Either Text ())
probeBackend backend url = do
  let (host, portTxt) = parseWsUrl url
      port = read (T.unpack portTxt) :: Int
      cfg = (defaultConfig backend)
              { substrateHost = T.unpack host
              , substratePort = port
              }
      method = backend <> ".hash"
  outcome <- try $ Transport.rpcCallWith cfg method Aeson.Null
  case outcome of
    Left (ex :: SomeException) -> pure $ Left $ T.pack (show ex)
    Right (Left err)           -> pure $ Left $ T.pack (show err)
    Right (Right _)            -> pure $ Right ()

-- | Determine which endpoints to wait for based on args.
resolveEndpoints :: WaitArgs -> IO (Either Text [(Text, Text)])
resolveEndpoints args = case waitBackend args of
  -- Explicit backend + URL: no config needed
  Just bk | Just url <- waitUrl args ->
    pure $ Right [(bk, url)]

  -- Explicit backend, URL from config
  Just bk -> do
    cfgResult <- loadSynapseConfig
    case cfgResult of
      Left err -> pure $ Left err
      Right sc -> do
        let allEps = collectEndpoints sc
        case lookup bk allEps of
          Just url -> pure $ Right [(bk, url)]
          Nothing  ->
            -- Backend not in config — try top-level URL as fallback
            let url = fromMaybe "ws://127.0.0.1:4444" (scUrl sc)
            in pure $ Right [(bk, url)]

  -- No backend specified: all from config
  Nothing -> do
    cfgResult <- loadSynapseConfig
    case cfgResult of
      Left err -> pure $ Left err
      Right sc -> do
        let eps = collectEndpoints sc
        if null eps
          then pure $ Left "no backends configured in synapse.config.json"
          else pure $ Right eps

-- | Run the wait command. Returns True on success, False on timeout/error.
runWait :: WaitArgs -> IO Bool
runWait args = do
  epResult <- resolveEndpoints args
  case epResult of
    Left err -> do
      logError err
      pure False

    Right endpoints -> do
      let n = length endpoints
          names = T.intercalate ", " [bk <> " (" <> url <> ")" | (bk, url) <- endpoints]
      logStep $ "Waiting for " <> T.pack (show n)
             <> " backend" <> (if n > 1 then "s" else "")
             <> ": " <> names
             <> " (timeout: " <> T.pack (show (waitTimeout args)) <> "s)"

      startTime <- getCurrentTime
      let timeoutSecs = fromIntegral (waitTimeout args) :: Double
          intervalUs  = waitInterval args * 1000

          loop :: Set Text -> IO Bool
          loop remaining
            | Set.null remaining = pure True
            | otherwise = do
                now <- getCurrentTime
                let elapsed = realToFrac (diffUTCTime now startTime) :: Double
                if elapsed >= timeoutSecs then do
                  logError $ "Timed out after " <> T.pack (show (waitTimeout args))
                          <> "s waiting for: "
                          <> T.intercalate ", " (Set.toList remaining)
                  pure False
                else do
                  newRemaining <- probeAll remaining endpoints
                  if Set.null newRemaining
                    then pure True
                    else do
                      threadDelay intervalUs
                      loop newRemaining

          probeAll :: Set Text -> [(Text, Text)] -> IO (Set Text)
          probeAll remaining eps = go remaining eps
            where
              go acc [] = pure acc
              go acc ((bk, url):rest)
                | not (Set.member bk acc) = go acc rest
                | otherwise = do
                    result <- probeBackend bk url
                    case result of
                      Right () -> do
                        logSuccess $ bk <> " reachable"
                        go (Set.delete bk acc) rest
                      Left _ -> do
                        when (waitDebug args) $
                          logInfo $ "  " <> bk <> " not yet reachable"
                        go acc rest

      let backendNames = Set.fromList [bk | (bk, _) <- endpoints]
      loop backendNames