packages feed

hinagata-cli-0.1.0.0: src/Hinagata/Cli/Validation.hs

-- | Bounded validation admission and command-wide cancellation.
module Hinagata.Cli.Validation (cancelledError, validateBatches) where

import Control.Concurrent.Async qualified as Async
import Control.Exception qualified
import Data.Text qualified as Text
import Hinagata.Cli.Output (CliError (..), ValidationResult (..))
import Hinagata.Cli.Process (CancellationScope, CommandCancelled (..), cancellationSignal, waitCancellation, withCooperativeCancellation)

cancelledError :: Int -> CliError
cancelledError signal = CliError "command" "command_cancelled" "command cancelled before completion" Nothing Nothing Nothing Nothing (128 + signal) Nothing Nothing Nothing Nothing

-- | A worker is joined even after cancellation. Its typed preparation result
-- takes precedence over a generic cancellation; completed results are retained.
validateBatches :: CancellationScope -> Int -> (Text.Text -> IO ValidationResult) -> [Text.Text] -> IO [ValidationResult]
validateBatches scope size validate names = withCooperativeCancellation scope (go (chunksOf size names))
  where
    cancelled name signal = ValidationResult name (Left (cancelledError signal))
    run name = Async.withAsync (validate name) $ \worker -> do
      raced <- Async.race (Async.waitCatch worker) (waitCancellation scope)
      result <- case raced of
        Left completed -> pure completed
        Right signal -> do
          Async.cancelWith worker (CommandCancelled signal)
          Async.waitCatch worker
      case result of
        Right value -> pure value
        Left exception -> case Control.Exception.fromException exception of
          Just (CommandCancelled signal) -> pure (cancelled name signal)
          Nothing -> Control.Exception.throwIO exception
    go [] = pure []
    go (batch : pending) = do
      signal <- cancellationSignal scope
      case signal of
        Just number -> pure (map (\name -> cancelled name number) (concat (batch : pending)))
        Nothing -> do
          results <- Async.mapConcurrently run batch
          rest <- go pending
          pure (results <> rest)

chunksOf :: Int -> [a] -> [[a]]
chunksOf _ [] = []
chunksOf size values = let (first, rest) = splitAt size values in first : chunksOf size rest