packages feed

atelier-core 0.8.1.0 → 1.0.0.0

raw patch · 67 files changed

+1508/−1125 lines, 67 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Atelier.Effects.Arguments: runArgumentsIO :: forall (es :: [Effect]) a. IOE :> es => Eff (Arguments ': es) a -> Eff es a
- Atelier.Effects.Cache: cacheDelete :: forall key value (es :: [Effect]). (HasCallStack, Cache key value :> es) => key -> Eff es ()
- Atelier.Effects.Cache: cacheInsert :: forall key value (es :: [Effect]). (HasCallStack, Cache key value :> es) => key -> value -> Eff es ()
- Atelier.Effects.Cache: cacheLookup :: forall key value (es :: [Effect]). (HasCallStack, Cache key value :> es) => key -> Eff es (Maybe value)
- Atelier.Effects.Cache: cacheModify :: forall key value (es :: [Effect]). (HasCallStack, Cache key value :> es) => key -> (Maybe value -> value) -> Eff es value
- Atelier.Effects.Cache: runCacheForever :: forall key value (es :: [Effect]) a. (Concurrent :> es, Hashable key) => Eff (Cache key value ': es) a -> Eff es a
- Atelier.Effects.Cache: runCacheTtl :: forall key value (es :: [Effect]) a. (Clock :> es, Conc :> es, Concurrent :> es, Delay :> es, Hashable key, Log :> es, Reader Config :> es) => Eff (Cache key value ': es) a -> Eff es a
- Atelier.Effects.Cache: runCacheTtlWithWait :: forall key value (es :: [Effect]) a. (Clock :> es, Conc :> es, Concurrent :> es, Hashable key, Log :> es, Reader Config :> es) => Eff es () -> Eff (Cache key value ': es) a -> Eff es a
- Atelier.Effects.Cache.Singleflight: [RemoveFromCache] :: forall key value (a :: Type -> Type). [key] -> Singleflight key value a ()
- Atelier.Effects.Cache.Singleflight: [UpdateCache] :: forall key value (a :: Type -> Type). [(key, value)] -> Singleflight key value a ()
- Atelier.Effects.Cache.Singleflight: [WithCache] :: forall key (a :: Type -> Type) value. Hashable key => key -> a value -> Singleflight key value a value
- Atelier.Effects.Cache.Singleflight: removeFromCache :: forall key value (es :: [Effect]). (HasCallStack, Singleflight key value :> es) => [key] -> Eff es ()
- Atelier.Effects.Cache.Singleflight: runSingleflight :: forall key value (es :: [Effect]) a. (Concurrent :> es, Hashable key) => Eff (Singleflight key value ': es) a -> Eff es a
- Atelier.Effects.Cache.Singleflight: updateCache :: forall key value (es :: [Effect]). (HasCallStack, Singleflight key value :> es) => [(key, value)] -> Eff es ()
- Atelier.Effects.Cache.Singleflight: withCache :: forall key value (es :: [Effect]). (HasCallStack, Singleflight key value :> es, Hashable key) => key -> Eff es value -> Eff es value
- Atelier.Effects.Chan: dupChan :: forall a (es :: [Effect]). Chan :> es => InChan a -> Eff es (OutChan a)
- Atelier.Effects.Chan: newChan :: forall a (es :: [Effect]). Chan :> es => Eff es (InChan a, OutChan a)
- Atelier.Effects.Chan: readChan :: forall a (es :: [Effect]). Chan :> es => OutChan a -> Eff es a
- Atelier.Effects.Chan: readChanBatched :: forall t a (es :: [Effect]). (Chan :> es, TimeUnit t, Timeout :> es) => t -> Int -> OutChan a -> Eff es (NonEmpty a)
- Atelier.Effects.Chan: runChan :: forall a (es :: [Effect]). IOE :> es => Eff (Chan ': es) a -> Eff es a
- Atelier.Effects.Chan: writeChan :: forall a (es :: [Effect]). Chan :> es => InChan a -> a -> Eff es ()
- Atelier.Effects.Clock: runClock :: forall (es :: [Effect]) a. IOE :> es => Eff (Clock ': es) a -> Eff es a
- Atelier.Effects.Clock: runClockConst :: forall (es :: [(Type -> Type) -> Type -> Type]) a. UTCTime -> Eff (Clock ': es) a -> Eff es a
- Atelier.Effects.Clock: runClockList :: forall (es :: [(Type -> Type) -> Type -> Type]) a. [UTCTime] -> Eff (Clock ': es) a -> Eff es a
- Atelier.Effects.Clock: runClockState :: forall (es :: [Effect]) a. State UTCTime :> es => Eff (Clock ': es) a -> Eff es a
- Atelier.Effects.Conc: concStrat :: UnliftStrategy
- Atelier.Effects.Conc: runConc :: forall (es :: [Effect]) a. (Concurrent :> es, IOE :> es) => Eff (Conc ': es) a -> Eff es a
- Atelier.Effects.Conc: runConcBase :: forall (es :: [Effect]) a. (Concurrent :> es, IOE :> es) => Scope -> Eff (Conc ': es) a -> Eff es a
- Atelier.Effects.Console: runConsole :: forall (es :: [Effect]) a. IOE :> es => Eff (Console ': es) a -> Eff es a
- Atelier.Effects.Console: runConsoleHandle :: forall (es :: [Effect]) a. IOE :> es => Handle -> Eff (Console ': es) a -> Eff es a
- Atelier.Effects.Console: runConsoleOut :: forall (es :: [Effect]) a. IOE :> es => Output -> Eff (Console ': es) a -> Eff es a
- Atelier.Effects.Console: runConsoleToList :: forall (es :: [(Type -> Type) -> Type -> Type]) a. Eff (Console ': es) a -> Eff es (a, [ByteString])
- Atelier.Effects.Debounce: debounced :: forall key (es :: [Effect]) a. Debounce key :> es => Millisecond -> key -> Eff es a -> Eff es ()
- Atelier.Effects.Debounce: debouncedWith :: forall arg key a (es :: [Effect]). (HasCallStack, Debounce key :> es, Typeable arg) => Millisecond -> (arg -> arg -> arg) -> key -> arg -> (arg -> Eff es a) -> Eff es ()
- Atelier.Effects.Debounce: runDebounce :: forall key (es :: [Effect]) a. (Conc :> es, Concurrent :> es, Delay :> es, Hashable key) => Eff (Debounce key ': es) a -> Eff es a
- Atelier.Effects.Debounce: runDebounceNoOp :: forall key (es :: [(Type -> Type) -> Type -> Type]) a. Eff (Debounce key ': es) a -> Eff es a
- Atelier.Effects.Delay: runDelay :: forall (es :: [Effect]) a. Concurrent :> es => Eff (Delay ': es) a -> Eff es a
- Atelier.Effects.Delay: runDelayNoOp :: forall (es :: [(Type -> Type) -> Type -> Type]) a. Eff (Delay ': es) a -> Eff es a
- Atelier.Effects.Env: getEnvironment :: forall (es :: [Effect]). (HasCallStack, Env :> es) => Eff es [(String, String)]
- Atelier.Effects.Env: lookupEnv :: forall (es :: [Effect]). (HasCallStack, Env :> es) => String -> Eff es (Maybe String)
- Atelier.Effects.Env: runEnv :: forall (es :: [Effect]) a. IOE :> es => Eff (Env ': es) a -> Eff es a
- Atelier.Effects.Env: runEnvConst :: forall (es :: [(Type -> Type) -> Type -> Type]) a. [(String, String)] -> Eff (Env ': es) a -> Eff es a
- Atelier.Effects.Exit: exitFailure :: forall (es :: [Effect]) a. Exit :> es => Eff es a
- Atelier.Effects.Exit: exitSuccess :: forall (es :: [Effect]) a. Exit :> es => Eff es a
- Atelier.Effects.Exit: exitWith :: forall a (es :: [Effect]). (HasCallStack, Exit :> es) => ExitCode -> Eff es a
- Atelier.Effects.Exit: runExit :: forall (es :: [Effect]) a. IOE :> es => Eff (Exit ': es) a -> Eff es a
- Atelier.Effects.Exit: runExitNoOp :: forall (es :: [(Type -> Type) -> Type -> Type]) a. Eff (Exit ': es) a -> Eff es (Either ExitCode a)
- Atelier.Effects.File: runFile :: forall (es :: [Effect]) a. (HasCallStack, IOE :> es) => Eff (File ': es) a -> Eff es a
- Atelier.Effects.File: withFile :: forall (es :: [Effect]) a. (File :> es, HasCallStack) => FilePath -> IOMode -> (Handle -> Eff es a) -> Eff es a
- Atelier.Effects.FileSystem: runFileSystemIO :: forall (es :: [Effect]) a. IOE :> es => Eff (FileSystem ': es) a -> Eff es a
- Atelier.Effects.FileSystem: runFileSystemNoOp :: forall (es :: [(Type -> Type) -> Type -> Type]) a. Eff (FileSystem ': es) a -> Eff es a
- Atelier.Effects.FileSystem: runFileSystemState :: forall (es :: [Effect]) a. State (Map FilePath ByteString) :> es => Eff (FileSystem ': es) a -> Eff es a
- Atelier.Effects.FileWatcher: Added :: FileEvent
- Atelier.Effects.FileWatcher: Modified :: FileEvent
- Atelier.Effects.FileWatcher: Removed :: FileEvent
- Atelier.Effects.FileWatcher: containing :: Text -> FilePath -> Bool
- Atelier.Effects.FileWatcher: data FileEvent
- Atelier.Effects.FileWatcher: data Watch
- Atelier.Effects.FileWatcher: dir :: FilePath -> Watch
- Atelier.Effects.FileWatcher: dirExt :: FilePath -> Text -> Watch
- Atelier.Effects.FileWatcher: dirWhere :: FilePath -> (FilePath -> Bool) -> Watch
- Atelier.Effects.FileWatcher: excluding :: Watch -> (FilePath -> Bool) -> Watch
- Atelier.Effects.FileWatcher: instance GHC.Classes.Eq Atelier.Effects.FileWatcher.FileEvent
- Atelier.Effects.FileWatcher: instance GHC.Show.Show Atelier.Effects.FileWatcher.FileEvent
- Atelier.Effects.FileWatcher: mergeFileEvent :: FileEvent -> FileEvent -> FileEvent
- Atelier.Effects.FileWatcher: runFileWatcherIO :: forall (es :: [Effect]) a. IOE :> es => Eff (FileWatcher ': es) a -> Eff es a
- Atelier.Effects.FileWatcher: runFileWatcherScripted :: forall (es :: [Effect]) a. Concurrent :> es => [(FilePath, FileEvent)] -> Eff (FileWatcher ': es) a -> Eff es a
- Atelier.Effects.Input: runInputConst :: forall i (es :: [(Type -> Type) -> Type -> Type]) a. i -> Eff (Input i ': es) a -> Eff es a
- Atelier.Effects.Input: runInputEff :: forall (es :: [Effect]) i a. Eff es i -> Eff (Input i ': es) a -> Eff es a
- Atelier.Effects.Log: runLog :: forall (es :: [Effect]) a. (Env :> es, IOE :> es, Reader Config :> es) => Eff (Log ': es) a -> Eff es a
- Atelier.Effects.Log: runLogNoOp :: forall (es :: [(Type -> Type) -> Type -> Type]) a. Eff (Log ': es) a -> Eff es a
- Atelier.Effects.Log: runLogToHandle :: forall (es :: [Effect]) a. IOE :> es => Handle -> Severity -> Eff (Log ': es) a -> Eff es a
- Atelier.Effects.Log: runLogWriter :: forall (es :: [Effect]) a. Writer [Message] :> es => Eff (Log ': es) a -> Eff es a
- Atelier.Effects.Posix.Daemons: runDaemons :: forall (es :: [Effect]) a. IOE :> es => Eff (Daemons ': es) a -> Eff es a
- Atelier.Effects.Process: runProcessIO :: forall (es :: [Effect]) a. IOE :> es => Eff (Process ': es) a -> Eff es a
- Atelier.Effects.PubSub.Pub: runNoOp :: forall event (es :: [(Type -> Type) -> Type -> Type]) a. Eff (Pub event ': es) a -> Eff es a
- Atelier.Effects.Publishing: runPubSub :: forall event (es :: [Effect]) a. (Chan :> es, Clock :> es) => Eff (Pub event ': (Sub event ': es)) a -> Eff es a
- Atelier.Effects.Tally: runTally :: forall key (es :: [Effect]) a. (Clock :> es, Conc :> es, Concurrent :> es, Delay :> es, Hashable key, Log :> es, Reader Config :> es) => Eff (Tally key ': es) a -> Eff es a
- Atelier.Effects.Tally: runTallyConst :: forall key (es :: [(Type -> Type) -> Type -> Type]) a. Int -> Eff (Tally key ': es) a -> Eff es a
- Atelier.Effects.Tally: withTallyCheck :: forall key status a (es :: [Effect]). (HasCallStack, Tally key :> es, Hashable key, Ord key) => key -> (Int -> status) -> (status -> Eff es a) -> Eff es a
- Atelier.Effects.Timeout: runTimeout :: forall (es :: [Effect]) a. (HasCallStack, IOE :> es) => Eff (Timeout ': es) a -> Eff es a
- Atelier.Effects.Timeout: timeout :: forall t a (es :: [Effect]). (HasCallStack, Timeout :> es, TimeUnit t) => t -> Eff es a -> Eff es (Maybe a)
- Atelier.Effects.Timeout: timeout_ :: forall t (es :: [Effect]) a. (TimeUnit t, Timeout :> es) => t -> Eff es a -> Eff es ()
- Atelier.Effects.UUID: data GenUUID (a :: Type -> Type) b
- Atelier.Effects.UUID: data UUID
- Atelier.Effects.UUID: gen :: forall (es :: [Effect]). (HasCallStack, GenUUID :> es) => Eff es UUID
- Atelier.Effects.UUID: runGenUUID :: forall (es :: [Effect]) a. IOE :> es => Eff (GenUUID ': es) a -> Eff es a
- Atelier.Effects.UUID: runGenUUIDConst :: forall (es :: [(Type -> Type) -> Type -> Type]) a. UUID -> Eff (GenUUID ': es) a -> Eff es a
- Atelier.Types.Base64: Base64 :: ByteString -> Base64
- Atelier.Types.Base64: [getBase64] :: Base64 -> ByteString
- Atelier.Types.Base64: instance Data.Aeson.Types.FromJSON.FromJSON Atelier.Types.Base64.Base64
- Atelier.Types.Base64: instance Data.Aeson.Types.ToJSON.ToJSON Atelier.Types.Base64.Base64
- Atelier.Types.Base64: newtype Base64
- Atelier.Types.JsonReadShow: JsonReadShow :: a -> JsonReadShow a
- Atelier.Types.JsonReadShow: [getJsonReadShow] :: JsonReadShow a -> a
- Atelier.Types.JsonReadShow: instance (GHC.Read.Read a, Data.Typeable.Internal.Typeable a) => Data.Aeson.Types.FromJSON.FromJSON (Atelier.Types.JsonReadShow.JsonReadShow a)
- Atelier.Types.JsonReadShow: instance GHC.Show.Show a => Data.Aeson.Types.ToJSON.ToJSON (Atelier.Types.JsonReadShow.JsonReadShow a)
- Atelier.Types.JsonReadShow: newtype JsonReadShow a
- Atelier.Types.QuietSnake: QuietSnake :: a -> QuietSnake a
- Atelier.Types.QuietSnake: [getQuietSnake] :: QuietSnake a -> a
- Atelier.Types.QuietSnake: newtype QuietSnake a
- Atelier.Types.WithDefaults: WithDefaults :: a -> WithDefaults a
- Atelier.Types.WithDefaults: [getWithDefaults] :: WithDefaults a -> a
- Atelier.Types.WithDefaults: instance (Data.Default.Internal.Default a, Data.Aeson.Types.FromJSON.FromJSON a, Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.FromJSON.FromJSON (Atelier.Types.WithDefaults.WithDefaults a)
- Atelier.Types.WithDefaults: newtype WithDefaults a
+ Atelier.Effects.Arguments: runIO :: forall (es :: [Effect]) a. IOE :> es => Eff (Arguments ': es) a -> Eff es a
+ Atelier.Effects.Arguments.Switch: parser :: Mod FlagFields Bool -> Parser Bool
+ Atelier.Effects.Cache: delete :: forall key value (es :: [Effect]). (HasCallStack, Cache key value :> es) => key -> Eff es ()
+ Atelier.Effects.Cache: insert :: forall key value (es :: [Effect]). (HasCallStack, Cache key value :> es) => key -> value -> Eff es ()
+ Atelier.Effects.Cache: lookup :: forall key value (es :: [Effect]). (HasCallStack, Cache key value :> es) => key -> Eff es (Maybe value)
+ Atelier.Effects.Cache: modify :: forall key value (es :: [Effect]). (HasCallStack, Cache key value :> es) => key -> (Maybe value -> value) -> Eff es value
+ Atelier.Effects.Cache: runForever :: forall key value (es :: [Effect]) a. (Concurrent :> es, Hashable key) => Eff (Cache key value ': es) a -> Eff es a
+ Atelier.Effects.Cache: runTTL :: forall key value (es :: [Effect]) a. (Clock :> es, Conc :> es, Concurrent :> es, Delay :> es, Hashable key, Log :> es, Reader Config :> es) => Eff (Cache key value ': es) a -> Eff es a
+ Atelier.Effects.Cache: runTTLWithWait :: forall key value (es :: [Effect]) a. (Clock :> es, Conc :> es, Concurrent :> es, Hashable key, Log :> es, Reader Config :> es) => Eff es () -> Eff (Cache key value ': es) a -> Eff es a
+ Atelier.Effects.Cache.Singleflight: [Remove] :: forall key value (a :: Type -> Type). [key] -> Singleflight key value a ()
+ Atelier.Effects.Cache.Singleflight: [Update] :: forall key value (a :: Type -> Type). [(key, value)] -> Singleflight key value a ()
+ Atelier.Effects.Cache.Singleflight: [With] :: forall key (a :: Type -> Type) value. Hashable key => key -> a value -> Singleflight key value a value
+ Atelier.Effects.Cache.Singleflight: remove :: forall key value (es :: [Effect]). (HasCallStack, Singleflight key value :> es) => [key] -> Eff es ()
+ Atelier.Effects.Cache.Singleflight: runIO :: forall key value (es :: [Effect]) a. (Concurrent :> es, Hashable key) => Eff (Singleflight key value ': es) a -> Eff es a
+ Atelier.Effects.Cache.Singleflight: update :: forall key value (es :: [Effect]). (HasCallStack, Singleflight key value :> es) => [(key, value)] -> Eff es ()
+ Atelier.Effects.Cache.Singleflight: with :: forall key value (es :: [Effect]). (HasCallStack, Singleflight key value :> es, Hashable key) => key -> Eff es value -> Eff es value
+ Atelier.Effects.Chan: dup :: forall a (es :: [Effect]). Chan :> es => InChan a -> Eff es (OutChan a)
+ Atelier.Effects.Chan: new :: forall a (es :: [Effect]). Chan :> es => Eff es (InChan a, OutChan a)
+ Atelier.Effects.Chan: read :: forall a (es :: [Effect]). Chan :> es => OutChan a -> Eff es a
+ Atelier.Effects.Chan: readBatched :: forall t a (es :: [Effect]). (Chan :> es, TimeUnit t, Timeout :> es) => t -> Int -> OutChan a -> Eff es (NonEmpty a)
+ Atelier.Effects.Chan: run :: forall a (es :: [Effect]). IOE :> es => Eff (Chan ': es) a -> Eff es a
+ Atelier.Effects.Chan: write :: forall a (es :: [Effect]). Chan :> es => InChan a -> a -> Eff es ()
+ Atelier.Effects.Clock: runConst :: forall (es :: [(Type -> Type) -> Type -> Type]) a. UTCTime -> Eff (Clock ': es) a -> Eff es a
+ Atelier.Effects.Clock: runIO :: forall (es :: [Effect]) a. IOE :> es => Eff (Clock ': es) a -> Eff es a
+ Atelier.Effects.Clock: runList :: forall (es :: [(Type -> Type) -> Type -> Type]) a. [UTCTime] -> Eff (Clock ': es) a -> Eff es a
+ Atelier.Effects.Clock: runState :: forall (es :: [Effect]) a. State UTCTime :> es => Eff (Clock ': es) a -> Eff es a
+ Atelier.Effects.Conc: runIO :: forall (es :: [Effect]) a. (Concurrent :> es, IOE :> es) => Eff (Conc ': es) a -> Eff es a
+ Atelier.Effects.Conc: runScope :: forall (es :: [Effect]) a. (Concurrent :> es, IOE :> es) => Scope -> Eff (Conc ': es) a -> Eff es a
+ Atelier.Effects.Conc: strategy :: UnliftStrategy
+ Atelier.Effects.Console: runHandle :: forall (es :: [Effect]) a. IOE :> es => Handle -> Eff (Console ': es) a -> Eff es a
+ Atelier.Effects.Console: runIO :: forall (es :: [Effect]) a. IOE :> es => Eff (Console ': es) a -> Eff es a
+ Atelier.Effects.Console: runOut :: forall (es :: [Effect]) a. IOE :> es => Output -> Eff (Console ': es) a -> Eff es a
+ Atelier.Effects.Console: runToList :: forall (es :: [(Type -> Type) -> Type -> Type]) a. Eff (Console ': es) a -> Eff es (a, [ByteString])
+ Atelier.Effects.Debounce: run :: forall key (es :: [Effect]) a. (Conc :> es, Concurrent :> es, Delay :> es, Hashable key) => Eff (Debounce key ': es) a -> Eff es a
+ Atelier.Effects.Debounce: runNoOp :: forall key (es :: [(Type -> Type) -> Type -> Type]) a. Eff (Debounce key ': es) a -> Eff es a
+ Atelier.Effects.Debounce: with :: forall key (es :: [Effect]) a. Debounce key :> es => Millisecond -> key -> Eff es a -> Eff es ()
+ Atelier.Effects.Debounce: withArg :: forall key (es :: [Effect]) arg a. (Debounce key :> es, Typeable arg) => Millisecond -> (arg -> arg -> arg) -> key -> arg -> (arg -> Eff es a) -> Eff es ()
+ Atelier.Effects.Delay: runIO :: forall (es :: [Effect]) a. Concurrent :> es => Eff (Delay ': es) a -> Eff es a
+ Atelier.Effects.Delay: runNoOp :: forall (es :: [(Type -> Type) -> Type -> Type]) a. Eff (Delay ': es) a -> Eff es a
+ Atelier.Effects.Env: all :: forall (es :: [Effect]). (HasCallStack, Env :> es) => Eff es [(String, String)]
+ Atelier.Effects.Env: lookup :: forall (es :: [Effect]). (HasCallStack, Env :> es) => String -> Eff es (Maybe String)
+ Atelier.Effects.Env: runConst :: forall (es :: [(Type -> Type) -> Type -> Type]) a. [(String, String)] -> Eff (Env ': es) a -> Eff es a
+ Atelier.Effects.Env: runIO :: forall (es :: [Effect]) a. IOE :> es => Eff (Env ': es) a -> Eff es a
+ Atelier.Effects.Exit: failure :: forall (es :: [Effect]) a. Exit :> es => Eff es a
+ Atelier.Effects.Exit: runIO :: forall (es :: [Effect]) a. IOE :> es => Eff (Exit ': es) a -> Eff es a
+ Atelier.Effects.Exit: runNoOp :: forall (es :: [(Type -> Type) -> Type -> Type]) a. Eff (Exit ': es) a -> Eff es (Either ExitCode a)
+ Atelier.Effects.Exit: success :: forall (es :: [Effect]) a. Exit :> es => Eff es a
+ Atelier.Effects.Exit: with :: forall a (es :: [Effect]). (HasCallStack, Exit :> es) => ExitCode -> Eff es a
+ Atelier.Effects.FSNotify: Added :: FilePath -> UTCTime -> EventIsDirectory -> Event
+ Atelier.Effects.FSNotify: CloseWrite :: FilePath -> UTCTime -> EventIsDirectory -> Event
+ Atelier.Effects.FSNotify: IsDirectory :: EventIsDirectory
+ Atelier.Effects.FSNotify: IsFile :: EventIsDirectory
+ Atelier.Effects.FSNotify: Modified :: FilePath -> UTCTime -> EventIsDirectory -> Event
+ Atelier.Effects.FSNotify: ModifiedAttributes :: FilePath -> UTCTime -> EventIsDirectory -> Event
+ Atelier.Effects.FSNotify: Removed :: FilePath -> UTCTime -> EventIsDirectory -> Event
+ Atelier.Effects.FSNotify: SingleThread :: ThreadingMode
+ Atelier.Effects.FSNotify: ThreadPerEvent :: ThreadingMode
+ Atelier.Effects.FSNotify: ThreadPerWatch :: ThreadingMode
+ Atelier.Effects.FSNotify: Unknown :: FilePath -> UTCTime -> EventIsDirectory -> String -> Event
+ Atelier.Effects.FSNotify: WatchModeOS :: WatchMode
+ Atelier.Effects.FSNotify: WatchModePoll :: Int -> WatchMode
+ Atelier.Effects.FSNotify: WatchedDirectoryRemoved :: FilePath -> UTCTime -> EventIsDirectory -> Event
+ Atelier.Effects.FSNotify: [eventIsDirectory] :: Event -> EventIsDirectory
+ Atelier.Effects.FSNotify: [eventPath] :: Event -> FilePath
+ Atelier.Effects.FSNotify: [eventString] :: Event -> String
+ Atelier.Effects.FSNotify: [eventTime] :: Event -> UTCTime
+ Atelier.Effects.FSNotify: [watchModePollInterval] :: WatchMode -> Int
+ Atelier.Effects.FSNotify: data Event
+ Atelier.Effects.FSNotify: data EventIsDirectory
+ Atelier.Effects.FSNotify: data FSNotify (a :: Type -> Type) b
+ Atelier.Effects.FSNotify: data ThreadingMode
+ Atelier.Effects.FSNotify: data WatchConfig
+ Atelier.Effects.FSNotify: data WatchManager
+ Atelier.Effects.FSNotify: data WatchMode
+ Atelier.Effects.FSNotify: defaultConfig :: WatchConfig
+ Atelier.Effects.FSNotify: run :: forall (es :: [Effect]) a. IOE :> es => Eff (FSNotify ': es) a -> Eff es a
+ Atelier.Effects.FSNotify: startManager :: forall (es :: [Effect]). FSNotify :> es => Eff es WatchManager
+ Atelier.Effects.FSNotify: startManagerConf :: forall (es :: [Effect]). FSNotify :> es => WatchConfig -> Eff es WatchManager
+ Atelier.Effects.FSNotify: stopManager :: forall (es :: [Effect]). FSNotify :> es => WatchManager -> Eff es ()
+ Atelier.Effects.FSNotify: type ActionPredicate = Event -> Bool
+ Atelier.Effects.FSNotify: type EventChannel = Chan Event
+ Atelier.Effects.FSNotify: type StopListening = IO ()
+ Atelier.Effects.FSNotify: watchDir :: forall (es :: [Effect]). FSNotify :> es => WatchManager -> FilePath -> ActionPredicate -> Action es -> Eff es StopListening
+ Atelier.Effects.FSNotify: watchDirChan :: forall (es :: [Effect]). FSNotify :> es => WatchManager -> FilePath -> ActionPredicate -> EventChannel -> Eff es StopListening
+ Atelier.Effects.FSNotify: watchTree :: forall (es :: [Effect]). FSNotify :> es => WatchManager -> FilePath -> ActionPredicate -> Action es -> Eff es StopListening
+ Atelier.Effects.FSNotify: watchTreeChan :: forall (es :: [Effect]). FSNotify :> es => WatchManager -> FilePath -> ActionPredicate -> EventChannel -> Eff es StopListening
+ Atelier.Effects.FSNotify: withManager :: forall (es :: [Effect]) a. FSNotify :> es => (WatchManager -> Eff es a) -> Eff es a
+ Atelier.Effects.FSNotify: withManagerConf :: forall (es :: [Effect]) a. FSNotify :> es => WatchConfig -> (WatchManager -> Eff es a) -> Eff es a
+ Atelier.Effects.FSNotify.Devel: allEvents :: (FilePath -> Bool) -> Event -> Bool
+ Atelier.Effects.FSNotify.Devel: doAllEvents :: Monad m => (FilePath -> m ()) -> Event -> m ()
+ Atelier.Effects.FSNotify.Devel: existsEvents :: (FilePath -> Bool) -> Event -> Bool
+ Atelier.Effects.FSNotify.Devel: treeExtAny :: forall (es :: [Effect]). FSNotify :> es => WatchManager -> FilePath -> Text -> (FilePath -> Eff es ()) -> Eff es StopListening
+ Atelier.Effects.FSNotify.Devel: treeExtExists :: forall (es :: [Effect]). FSNotify :> es => WatchManager -> FilePath -> Text -> (FilePath -> Eff es ()) -> Eff es StopListening
+ Atelier.Effects.File: run :: forall (es :: [Effect]) a. (HasCallStack, IOE :> es) => Eff (File ': es) a -> Eff es a
+ Atelier.Effects.File: with :: forall (es :: [Effect]) a. (File :> es, HasCallStack) => FilePath -> IOMode -> (Handle -> Eff es a) -> Eff es a
+ Atelier.Effects.FileSystem: [MakeAbsolute] :: forall (a :: Type -> Type). FilePath -> FileSystem a [Char]
+ Atelier.Effects.FileSystem: makeAbsolute :: forall (es :: [Effect]). (HasCallStack, FileSystem :> es) => FilePath -> Eff es FilePath
+ Atelier.Effects.FileSystem: runIO :: forall (es :: [Effect]) a. IOE :> es => Eff (FileSystem ': es) a -> Eff es a
+ Atelier.Effects.FileSystem: runNoOp :: forall (es :: [(Type -> Type) -> Type -> Type]) a. Eff (FileSystem ': es) a -> Eff es a
+ Atelier.Effects.FileSystem: runState :: forall (es :: [Effect]) a. State (Map FilePath ByteString) :> es => Eff (FileSystem ': es) a -> Eff es a
+ Atelier.Effects.FileWatcher: runIO :: forall (es :: [Effect]) a. (Delay :> es, FSNotify :> es, FileSystem :> es) => Eff (FileWatcher ': es) a -> Eff es a
+ Atelier.Effects.FileWatcher: runScripted :: forall (es :: [Effect]) a. Concurrent :> es => [(FilePath, Event)] -> Eff (FileWatcher ': es) a -> Eff es a
+ Atelier.Effects.FileWatcher.Event: Added :: Event
+ Atelier.Effects.FileWatcher.Event: Modified :: Event
+ Atelier.Effects.FileWatcher.Event: Removed :: Event
+ Atelier.Effects.FileWatcher.Event: data Event
+ Atelier.Effects.FileWatcher.Event: fromFS :: Event -> Event
+ Atelier.Effects.FileWatcher.Event: instance GHC.Base.Semigroup Atelier.Effects.FileWatcher.Event.Event
+ Atelier.Effects.FileWatcher.Event: instance GHC.Classes.Eq Atelier.Effects.FileWatcher.Event.Event
+ Atelier.Effects.FileWatcher.Event: instance GHC.Show.Show Atelier.Effects.FileWatcher.Event.Event
+ Atelier.Effects.FileWatcher.Event: merge :: Event -> Event -> Event
+ Atelier.Effects.FileWatcher.Watch: containing :: Text -> FilePath -> Bool
+ Atelier.Effects.FileWatcher.Watch: data Watch
+ Atelier.Effects.FileWatcher.Watch: dir :: FilePath -> Watch
+ Atelier.Effects.FileWatcher.Watch: dirExt :: FilePath -> Text -> Watch
+ Atelier.Effects.FileWatcher.Watch: dirWhere :: FilePath -> (FilePath -> Bool) -> Watch
+ Atelier.Effects.FileWatcher.Watch: excluding :: Watch -> (FilePath -> Bool) -> Watch
+ Atelier.Effects.FileWatcher.Watch: isStrictAncestor :: FilePath -> FilePath -> Bool
+ Atelier.Effects.FileWatcher.Watch: makeAbsolute :: forall (es :: [Effect]). FileSystem :> es => Watch -> Eff es Watch
+ Atelier.Effects.FileWatcher.Watch: matches :: FilePath -> Watch -> Bool
+ Atelier.Effects.FileWatcher.Watch: path :: Watch -> FilePath
+ Atelier.Effects.GenUUID: data GenUUID (a :: Type -> Type) b
+ Atelier.Effects.GenUUID: data UUID
+ Atelier.Effects.GenUUID: gen :: forall (es :: [Effect]). (HasCallStack, GenUUID :> es) => Eff es UUID
+ Atelier.Effects.GenUUID: runConst :: forall (es :: [(Type -> Type) -> Type -> Type]) a. UUID -> Eff (GenUUID ': es) a -> Eff es a
+ Atelier.Effects.GenUUID: runIO :: forall (es :: [Effect]) a. IOE :> es => Eff (GenUUID ': es) a -> Eff es a
+ Atelier.Effects.Log: runIO :: forall (es :: [Effect]) a. (Env :> es, IOE :> es, Reader Config :> es) => Eff (Log ': es) a -> Eff es a
+ Atelier.Effects.Log: runNoOp :: forall (es :: [(Type -> Type) -> Type -> Type]) a. Eff (Log ': es) a -> Eff es a
+ Atelier.Effects.Log: runToHandle :: forall (es :: [Effect]) a. IOE :> es => Handle -> Severity -> Eff (Log ': es) a -> Eff es a
+ Atelier.Effects.Log: runWriter :: forall (es :: [Effect]) a. Writer [Message] :> es => Eff (Log ': es) a -> Eff es a
+ Atelier.Effects.Posix.Daemons: run :: forall (es :: [Effect]) a. IOE :> es => Eff (Daemons ': es) a -> Eff es a
+ Atelier.Effects.Process: run :: forall (es :: [Effect]) a. IOE :> es => Eff (Process ': es) a -> Eff es a
+ Atelier.Effects.Process.Internal: [getRunningProcess] :: RunningProcess i o e -> Process i o e
+ Atelier.Effects.PubSub.Pub: noOp :: forall event (es :: [(Type -> Type) -> Type -> Type]) a. Eff (Pub event ': es) a -> Eff es a
+ Atelier.Effects.Tally: count :: forall key a (es :: [Effect]). (HasCallStack, Tally key :> es, Hashable key, Ord key) => key -> (Int -> Eff es a) -> Eff es a
+ Atelier.Effects.Tally: run :: forall key (es :: [Effect]) a. (Clock :> es, Conc :> es, Concurrent :> es, Delay :> es, Hashable key, Log :> es, Reader Config :> es) => Eff (Tally key ': es) a -> Eff es a
+ Atelier.Effects.Tally: runConst :: forall key (es :: [(Type -> Type) -> Type -> Type]) a. Int -> Eff (Tally key ': es) a -> Eff es a
+ Atelier.Effects.Timeout: run :: forall (es :: [Effect]) a. (HasCallStack, IOE :> es) => Eff (Timeout ': es) a -> Eff es a
+ Atelier.Effects.Timeout: with :: forall t (es :: [Effect]) a. (TimeUnit t, Timeout :> es) => t -> Eff es a -> Eff es (Maybe a)
+ Atelier.Effects.Timeout: with_ :: forall t (es :: [Effect]) a. (TimeUnit t, Timeout :> es) => t -> Eff es a -> Eff es ()
+ Atelier.Types.JSON.Base64: Base64 :: ByteString -> Base64
+ Atelier.Types.JSON.Base64: [getBase64] :: Base64 -> ByteString
+ Atelier.Types.JSON.Base64: instance Data.Aeson.Types.FromJSON.FromJSON Atelier.Types.JSON.Base64.Base64
+ Atelier.Types.JSON.Base64: instance Data.Aeson.Types.ToJSON.ToJSON Atelier.Types.JSON.Base64.Base64
+ Atelier.Types.JSON.Base64: newtype Base64
+ Atelier.Types.JSON.Renamed: data Renamed (from :: Symbol) (to :: Symbol)
+ Atelier.Types.JSON.Renamed: instance (GHC.TypeLits.KnownSymbol from, GHC.TypeLits.KnownSymbol to) => Data.Aeson.Types.FromJSON.FromJSON (Atelier.Types.JSON.Renamed.Renamed from to)
+ Atelier.Types.JSON.Renamed: instance Data.Aeson.Types.ToJSON.ToJSON (Atelier.Types.JSON.Renamed.Renamed from to)
+ Atelier.Types.JSON.Renamed: instance GHC.Classes.Eq (Atelier.Types.JSON.Renamed.Renamed from to)
+ Atelier.Types.JSON.Renamed: instance GHC.Generics.Generic (Atelier.Types.JSON.Renamed.Renamed from to)
+ Atelier.Types.JSON.Renamed: instance GHC.Show.Show (Atelier.Types.JSON.Renamed.Renamed from to)
+ Atelier.Types.JSON.WithDefaults: WithDefaults :: a -> WithDefaults a
+ Atelier.Types.JSON.WithDefaults: [getWithDefaults] :: WithDefaults a -> a
+ Atelier.Types.JSON.WithDefaults: instance (Data.Default.Internal.Default a, Data.Aeson.Types.FromJSON.FromJSON a, Data.Aeson.Types.ToJSON.ToJSON a) => Data.Aeson.Types.FromJSON.FromJSON (Atelier.Types.JSON.WithDefaults.WithDefaults a)
+ Atelier.Types.JSON.WithDefaults: newtype WithDefaults a
- Atelier.Effects.Arguments: execParser :: forall a (es :: [Effect]). (HasCallStack, Arguments :> es) => ParserInfo a -> Eff es a
+ Atelier.Effects.Arguments: execParser :: forall (es :: [Effect]) a. Arguments :> es => ParserInfo a -> Eff es a
- Atelier.Effects.FileWatcher: watchFilePaths :: forall a (es :: [Effect]). (HasCallStack, FileWatcher :> es) => [Watch] -> (FilePath -> FileEvent -> Eff es a) -> Eff es Void
+ Atelier.Effects.FileWatcher: watchFilePaths :: forall a (es :: [Effect]). (HasCallStack, FileWatcher :> es) => [Watch] -> (FilePath -> Event -> Eff es a) -> Eff es Void
- Atelier.Effects.FileWatcher: watchFilePathsDebounced :: forall (es :: [Effect]). (Debounce FilePath :> es, FileWatcher :> es) => [Watch] -> (FilePath -> FileEvent -> Eff es ()) -> Eff es Void
+ Atelier.Effects.FileWatcher: watchFilePathsDebounced :: forall (es :: [Effect]). (Debounce FilePath :> es, FileWatcher :> es) => [Watch] -> (FilePath -> Event -> Eff es ()) -> Eff es Void
- Atelier.Effects.Posix.Daemons: daemonize :: forall (es :: [Effect]). (HasCallStack, Daemons :> es) => PidFile -> Eff es () -> Eff es ()
+ Atelier.Effects.Posix.Daemons: daemonize :: forall (es :: [Effect]). Daemons :> es => PidFile -> Eff es () -> Eff es ()
- Atelier.Effects.Posix.Daemons: forceKillAndWait :: forall (es :: [Effect]). (HasCallStack, Daemons :> es) => PidFile -> Eff es (Either SomeException ())
+ Atelier.Effects.Posix.Daemons: forceKillAndWait :: forall (es :: [Effect]). Daemons :> es => PidFile -> Eff es (Either SomeException ())
- Atelier.Effects.Posix.Daemons: isRunning :: forall (es :: [Effect]). (HasCallStack, Daemons :> es) => PidFile -> Eff es Bool
+ Atelier.Effects.Posix.Daemons: isRunning :: forall (es :: [Effect]). Daemons :> es => PidFile -> Eff es Bool
- Atelier.Effects.Process: getExecutablePath :: forall (es :: [Effect]). (HasCallStack, Process :> es) => Eff es FilePath
+ Atelier.Effects.Process: getExecutablePath :: forall (es :: [Effect]). Process :> es => Eff es FilePath
- Atelier.Effects.Process: interruptProcessGroup :: forall i o e (es :: [Effect]). (HasCallStack, Process :> es) => RunningProcess i o e -> Eff es ()
+ Atelier.Effects.Process: interruptProcessGroup :: forall (es :: [Effect]) i o e. Process :> es => RunningProcess i o e -> Eff es ()
- Atelier.Effects.Process: readProcess :: forall i o e (es :: [Effect]). (HasCallStack, Process :> es) => ProcessConfig i o e -> Eff es (ExitCode, LByteString, LByteString)
+ Atelier.Effects.Process: readProcess :: forall (es :: [Effect]) i o e. Process :> es => ProcessConfig i o e -> Eff es (ExitCode, LByteString, LByteString)
- Atelier.Effects.Process: readProcessStdout :: forall i o e (es :: [Effect]). (HasCallStack, Process :> es) => ProcessConfig i o e -> Eff es (ExitCode, LByteString)
+ Atelier.Effects.Process: readProcessStdout :: forall (es :: [Effect]) i o e. Process :> es => ProcessConfig i o e -> Eff es (ExitCode, LByteString)
- Atelier.Effects.Process: runProcess :: forall i o e (es :: [Effect]). (HasCallStack, Process :> es) => ProcessConfig i o e -> Eff es ExitCode
+ Atelier.Effects.Process: runProcess :: forall (es :: [Effect]) i o e. Process :> es => ProcessConfig i o e -> Eff es ExitCode
- Atelier.Effects.Process: waitExitCode :: forall i o e (es :: [Effect]). (HasCallStack, Process :> es) => RunningProcess i o e -> Eff es ExitCode
+ Atelier.Effects.Process: waitExitCode :: forall (es :: [Effect]) i o e. Process :> es => RunningProcess i o e -> Eff es ExitCode

Files

CHANGELOG.md view
@@ -7,6 +7,74 @@  ## [Unreleased] +## [1.0.0.0] - 2026-10-29++### Added++- `Atelier.Effects.FSNotify`: `System.FSNotify` lifted to `Eff`.+- `Atelier.Types.JSON.Renamed`: used to mark properties that have been renamed.+  Attempting to JSON parse a property that is `Renamed "from" "to"` will+  produce an error message denoting that the property `"from"` has been renamed+  to `"to"`.+- `Atelier.Effects.Arguments.Switch`: similar to `Options.Applicative.switch`+  from `optparse-applicative`, but includes an inverse flag in the form of+  `--no-<long-name>` for the flag.++### Changed++- `Atelier.Effects.Arguments`: converted to a static effect.+- `Atelier.Effects.Arguments.runArgumentsIO` renamed to+  `Atelier.Effects.Arguments.runIO`.+- `Atelier.Types.Base64` moved to `Atelier.Types.JSON.Base64`.+- `Atelier.Types.WithDefaults` moved to `Atelier.Types.JSON.WithDefaults`.+- `Atelier.Effects.Cache`: renamed members to make more suited for qualified+  use.+- `Atelier.Effects.Chan`: renamed members to make more suited for qualified+  use.+- `Atelier.Effects.Clock`: renamed members to make more suited for qualified+  use.+- `Atelier.Effects.Conc`: renamed members to make more suited for qualified+  use.+- `Atelier.Effects.Console`: renamed members to make more suited for qualified+  use.+- `Atelier.Effects.Debounce`: renamed members to make more suited for qualified+  use.+- `Atelier.Effects.Delay`: renamed members to make more suited for qualified+  use.+- `Atelier.Effects.Env`: renamed members to make more suited for qualified use.+- `Atelier.Effects.Exit`: renamed members to make more suited for qualified+  use.+- `Atelier.Effects.File`: renamed members to make more suited for qualified+  use.+- `Atelier.Effects.FileSystem`: renamed members to make more suited for+  qualified use.+- `Atelier.Effects.FileWatcher`: renamed members to make more suited for+  qualified use.+- `Atelier.Effects.Log`: renamed members to make more suited for qualified use.+- `Atelier.Effects.Process`: converted to static effect, and renamed members to+  make more suited for qualified use.+- `Atelier.Effects.Tally`: renamed members to make more suited for qualified+  use.+- `Atelier.Effects.Tally.count` (previously+  `Atelier.Effects.Tally.withTallyCheck`) now no longer takes a classifier. The+  classifier function was always just ran and passed straight through to the+  continuation, so now the continuation just receives the count as-is.+- `Atelier.Effects.Timeout`: converted to static effect, and renamed members to+  make more suited for qualified use.+- `Atelier.Effects.UUID`: renamed to `Atelier.Effects.GenUUID`, and renamed+  members to make more suited for qualified use.+- `Atelier.Effects.Cache.Singleflight`: renamed members to make more suited for+  qualified use.+- `Atelier.Effects.Posix.Daemons`: converted to static effect, and renamed members to+  make more suited for qualified use.+- `Atelier.Effects.PubSub.Pub`: `runNoOp` renamed to `noOp`.++### Removed++- Deprecated functions in `Atelier.Effects.Input`.+- Deprecated modules `Atelier.Effects.Publishing`,+  `Atelier.Effects.Publishing.Sub` and `Atelier.Effects.Publishing.Pub`.+ ## [0.8.1.0] - 2026-10-23  ### Added
atelier-core.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           atelier-core-version:        0.8.1.0+version:        1.0.0.0 synopsis:       Foundational Effectful-based effects and utilities description:    Core effects and utilities for effect-based applications, built on Effectful — part of the atelier toolkit. category:       Control@@ -34,6 +34,7 @@   exposed-modules:       Atelier.Config       Atelier.Effects.Arguments+      Atelier.Effects.Arguments.Switch       Atelier.Effects.Await       Atelier.Effects.Cache       Atelier.Effects.Cache.Config@@ -50,6 +51,11 @@       Atelier.Effects.FileSystem       Atelier.Effects.FileSystem.Glob       Atelier.Effects.FileWatcher+      Atelier.Effects.FileWatcher.Event+      Atelier.Effects.FileWatcher.Watch+      Atelier.Effects.FSNotify+      Atelier.Effects.FSNotify.Devel+      Atelier.Effects.GenUUID       Atelier.Effects.Input       Atelier.Effects.Internal.Coroutine       Atelier.Effects.Iterator@@ -58,30 +64,25 @@       Atelier.Effects.Posix.IO       Atelier.Effects.Process       Atelier.Effects.Process.Internal-      Atelier.Effects.Publishing-      Atelier.Effects.Publishing.Pub-      Atelier.Effects.Publishing.Sub       Atelier.Effects.PubSub       Atelier.Effects.PubSub.Pub       Atelier.Effects.PubSub.Sub       Atelier.Effects.Reader       Atelier.Effects.Tally       Atelier.Effects.Timeout-      Atelier.Effects.UUID       Atelier.Effects.Yield       Atelier.Exception       Atelier.Signal       Atelier.Time-      Atelier.Types.Base64       Atelier.Types.HttpApiDataReadShow+      Atelier.Types.JSON.Base64       Atelier.Types.JSON.QuietSnake       Atelier.Types.JSON.ReadShow+      Atelier.Types.JSON.Renamed       Atelier.Types.JSON.StringLiteral-      Atelier.Types.JsonReadShow-      Atelier.Types.QuietSnake+      Atelier.Types.JSON.WithDefaults       Atelier.Types.Semaphore       Atelier.Types.Semaphore.STM-      Atelier.Types.WithDefaults   other-modules:       Paths_atelier_core   autogen-modules:@@ -155,6 +156,7 @@   main-is: Driver.hs   other-modules:       Unit.Atelier.ConfigSpec+      Unit.Atelier.Effects.Arguments.SwitchSpec       Unit.Atelier.Effects.AwaitSpec       Unit.Atelier.Effects.Cache.SingleflightSpec       Unit.Atelier.Effects.CacheSpec@@ -171,9 +173,9 @@       Unit.Atelier.Effects.PubSubSpec       Unit.Atelier.Effects.TallySpec       Unit.Atelier.Effects.YieldSpec+      Unit.Atelier.Types.JSON.WithDefaultsSpec       Unit.Atelier.Types.Semaphore.STMSpec       Unit.Atelier.Types.SemaphoreSpec-      Unit.Atelier.Types.WithDefaultsSpec       Paths_atelier_core   autogen-modules:       Paths_atelier_core@@ -211,6 +213,7 @@     , effectful-core ==2.7.*     , effectful-plugin ==2.2.*     , hedgehog ==1.7.*+    , optparse-applicative ==0.19.*     , stm ==2.5.*     , stm-containers ==1.2.*     , tasty ==1.5.*
src/Atelier/Config.hs view
@@ -59,15 +59,17 @@ import Data.Text qualified as T import Data.Text.Encoding qualified as TE -import Atelier.Effects.Env (Env, getEnvironment)+import Atelier.Effects.Env (Env) +import Atelier.Effects.Env qualified as Env + -- | Build a nested JSON Object from environment variables with the given prefix. -- Variables are expected in the form PREFIX__SEG1__SEG2=value. -- Double underscore is the path separator; single underscore is preserved within segments. envOverrides :: (Env :> es) => Text -> Eff es Value envOverrides prefix = do-    allEnv <- getEnvironment+    allEnv <- Env.all     let prefixStr = toString prefix <> "__"         matching = [(k, v) | (k, v) <- allEnv, prefixStr `isPrefixOf` k]         pairs = [(splitPath (drop (length prefixStr) k), parseScalar (toText v)) | (k, v) <- matching]
src/Atelier/Effects/Arguments.hs view
@@ -1,3 +1,5 @@+{-# OPTIONS_GHC -Wno-redundant-constraints #-}+ -- | Effect for parsing command-line arguments. -- -- A thin wrapper over @optparse-applicative@'s 'Opt.execParser', so callers@@ -13,28 +15,32 @@     , execParser        -- * Interpreters-    , runArgumentsIO+    , runIO     ) where -import Effectful (Effect, IOE)-import Effectful.Dispatch.Dynamic (interpret_)-import Effectful.TH (makeEffect)+import Effectful (Dispatch (..), DispatchOf, Effect, IOE)+import Effectful.Dispatch.Static (SideEffects (..), StaticRep, evalStaticRep, unsafeEff_) import Options.Applicative (ParserInfo)  import Options.Applicative qualified as Opt   -- | Reading and parsing command-line arguments.-data Arguments :: Effect where-    -- | Read @argv@ and parse it with the given parser, printing usage and-    -- exiting on failure (see @optparse-applicative@'s 'Opt.execParser').-    ExecParser :: ParserInfo a -> Arguments m a+data Arguments :: Effect  -makeEffect ''Arguments+type instance DispatchOf Arguments = Static WithSideEffects  -runArgumentsIO :: (IOE :> es) => Eff (Arguments : es) a -> Eff es a-runArgumentsIO = interpret_ \case-    ExecParser pinfo -> liftIO $ Opt.execParser pinfo+data instance StaticRep Arguments = Arguments+++-- \| Read @argv@ and parse it with the given parser, printing usage and+-- exiting on failure (see @optparse-applicative@'s 'Opt.execParser').+execParser :: (Arguments :> es) => ParserInfo a -> Eff es a+execParser = unsafeEff_ . Opt.execParser+++runIO :: (IOE :> es) => Eff (Arguments : es) a -> Eff es a+runIO = evalStaticRep Arguments
+ src/Atelier/Effects/Arguments/Switch.hs view
@@ -0,0 +1,58 @@+module Atelier.Effects.Arguments.Switch (parser) where++import Options.Applicative (FlagFields, Parser, flag', internal, long, style)+import Options.Applicative.Builder.Internal (FlagFields (..), Mod (..))+import Options.Applicative.Types (OptName (..))++import Data.List qualified as List+import Options.Applicative.Help qualified as Doc+++-- | Builder for a switch that has an inverse option as well in the form of+-- `--no-<flag-name>`. Automatically finds the long name and short name among+-- the options passed. If no long name is set, then `--no-<flag-name>` will not+-- be made available, and this parser behaves just like the regular `switch`+-- parser builder from `optparse-applicative`. The flags may be repeated, and+-- the last one given wins.+parser+    :: Mod FlagFields Bool+    -- ^ Extra flag options to use.+    -> Parser Bool+parser opts =+    fmap (maybe False last . nonEmpty)+        $ many+        $ maybe empty inactiveParser longName+            <|> activeParser shortName longName opts+  where+    Mod f _ _ = opts+    realOpts = f $ FlagFields [] False+    shortName =+        viaNonEmpty head+            $ mapMaybe+                ( \case+                    OptShort c -> Just c+                    _ -> Nothing+                )+                realOpts.flagNames+    longName =+        viaNonEmpty head+            $ mapMaybe+                ( \case+                    OptLong l -> Just l+                    _ -> Nothing+                )+                realOpts.flagNames+++activeParser :: Maybe Char -> Maybe String -> Mod FlagFields Bool -> Parser Bool+activeParser shortName longName opts =+    flag' True $ opts <> style (const displayName)+  where+    displayName = mconcat $ Doc.punctuate "," $ displayedShortName <> displayedLongName+    displayedShortName = maybe [] (one . Doc.pretty . ("-" <>) . List.singleton) shortName+    displayedLongName = maybe [] (one . Doc.pretty . ("--[no-]" <>)) longName+++inactiveParser :: String -> Parser Bool+inactiveParser longName =+    flag' False $ long ("no-" <> longName) <> internal
src/Atelier/Effects/Cache.hs view
@@ -2,18 +2,18 @@ module Atelier.Effects.Cache     ( -- * Effect       Cache-    , cacheLookup-    , cacheInsert-    , cacheDelete-    , cacheModify+    , lookup+    , insert+    , delete+    , modify        -- * Re-exports     , module Atelier.Effects.Cache.Config        -- * Interpreters-    , runCacheTtl-    , runCacheTtlWithWait-    , runCacheForever+    , runTTL+    , runTTLWithWait+    , runForever     ) where @@ -46,14 +46,14 @@ -- | Effect for a generic key-value cache. data Cache key value :: Effect where     -- | Look up a key, returning 'Nothing' if it is absent.-    CacheLookup :: key -> Cache key value m (Maybe value)+    Lookup :: key -> Cache key value m (Maybe value)     -- | Insert or overwrite the value at a key.-    CacheInsert :: key -> value -> Cache key value m ()+    Insert :: key -> value -> Cache key value m ()     -- | Remove a key from the cache.-    CacheDelete :: key -> Cache key value m ()+    Delete :: key -> Cache key value m ()     -- | Apply a function to a key's current value (or its absence), store the     -- result, and return it.-    CacheModify :: key -> (Maybe value -> value) -> Cache key value m value+    Modify :: key -> (Maybe value -> value) -> Cache key value m value   makeEffect ''Cache@@ -71,7 +71,7 @@ -- Entries are evicted after @entryTtl@ from their first insertion. -- Subsequent updates to the same key preserve the original timestamp. -- A background thread runs every @cleanupInterval@ to remove expired entries.-runCacheTtl+runTTL     :: forall key value es a      . ( Clock :> es        , Conc :> es@@ -82,16 +82,16 @@        , Reader Config :> es        )     => Eff (Cache key value : es) a -> Eff es a-runCacheTtl act = do+runTTL act = do     cfg <- ask-    runCacheTtlWithWait (Delay.wait $ nominalDiffTime @Microsecond cfg.cleanupInterval) act+    runTTLWithWait (Delay.wait $ nominalDiffTime @Microsecond cfg.cleanupInterval) act  --- | Like 'runCacheTtl', but with the cleanup cadence supplied explicitly as a+-- | Like 'runTtl', but with the cleanup cadence supplied explicitly as a -- waiting action rather than read from 'Config'. Each time the action -- completes, expired entries are swept. Useful for tests that drive cleanup -- deterministically.-runCacheTtlWithWait+runTTLWithWait     :: forall key value es a      . ( Clock :> es        , Conc :> es@@ -104,7 +104,7 @@     -- ^ Action that completes when it is time to perform cleanup.     -> Eff (Cache key value : es) a     -> Eff es a-runCacheTtlWithWait waitForNextCleanup action = do+runTTLWithWait waitForNextCleanup action = do     cfg <- ask @Config     store <- STM.atomically (Map.new :: STM (Map key (CacheEntry value))) @@ -118,9 +118,9 @@             $ "Evicted " <> show evicted <> " entries"      interpretWith action $ \_ -> \case-        CacheLookup key ->+        Lookup key ->             fmap (.value) <$> STM.atomically (Map.lookup key store)-        CacheInsert key val -> do+        Insert key val -> do             now <- currentTime             STM.atomically $ do                 existing <- Map.lookup key store@@ -128,9 +128,9 @@                         Nothing -> CacheEntry {value = val, createdAt = now}                         Just e -> e {value = val} -- preserve original timestamp                 Map.insert entry key store-        CacheDelete key ->+        Delete key ->             STM.atomically $ Map.delete key store-        CacheModify key f -> do+        Modify key f -> do             now <- currentTime             STM.atomically $ do                 existing <- Map.lookup key store@@ -164,21 +164,21 @@ -- -- Uses a plain STM 'Map' with no eviction. Useful for tests and -- session-scoped caches where eviction is never needed.-runCacheForever+runForever     :: forall key value es a      . (Concurrent :> es, Hashable key)     => Eff (Cache key value : es) a     -> Eff es a-runCacheForever action = do+runForever action = do     store <- STM.atomically (Map.new :: STM (Map key value))     interpretWith action $ \_ -> \case-        CacheLookup key ->+        Lookup key ->             STM.atomically $ Map.lookup key store-        CacheInsert key val ->+        Insert key val ->             STM.atomically $ Map.insert val key store-        CacheDelete key ->+        Delete key ->             STM.atomically $ Map.delete key store-        CacheModify key f ->+        Modify key f ->             STM.atomically $ do                 existing <- Map.lookup key store                 let newVal = f existing
src/Atelier/Effects/Cache/Singleflight.hs view
@@ -7,10 +7,10 @@ -- be seeded with 'updateCache' or invalidated with 'removeFromCache'. module Atelier.Effects.Cache.Singleflight     ( Singleflight (..)-    , withCache-    , updateCache-    , removeFromCache-    , runSingleflight+    , with+    , update+    , remove+    , runIO     ) where @@ -36,12 +36,12 @@ data Singleflight key value :: Effect where     -- | Execute a computation with singleflight semantics     -- If another computation for the same key is in-flight, wait for its result-    WithCache :: (Hashable key) => key -> m value -> Singleflight key value m value+    With :: (Hashable key) => key -> m value -> Singleflight key value m value     -- | Pre-populate the cache with known values     -- Useful when values are already available to avoid redundant computation-    UpdateCache :: [(key, value)] -> Singleflight key value m ()+    Update :: [(key, value)] -> Singleflight key value m ()     -- | Remove keys from the cache so future requests recompute from source-    RemoveFromCache :: [key] -> Singleflight key value m ()+    Remove :: [key] -> Singleflight key value m ()   makeEffect ''Singleflight@@ -51,18 +51,18 @@   -- | Run the Singleflight effect with an in-memory cache-runSingleflight+runIO     :: forall key value es a      . (Concurrent :> es, Hashable key)     => Eff (Singleflight key value : es) a     -> Eff es a-runSingleflight action = do+runIO action = do     -- Initialize the cache     cache :: InFlightMap key value <- STM.atomically Map.new      -- Run with the cache in Reader context, interpreting Singleflight operations     interpretWith action $ \env -> \case-        WithCache key computation -> localSeqUnlift env $ \unlift -> do+        With key computation -> localSeqUnlift env $ \unlift -> do             -- Singleflight pattern: check if computation is already in-flight.             -- Also attempt a non-blocking read of any existing result in the same transaction.             (mvar, isFirst, mResult) <- STM.atomically $ do@@ -115,7 +115,7 @@                     case result of                         Left exception -> throwIO exception                         Right value -> pure value-        UpdateCache entries -> do+        Update entries -> do             STM.atomically $ do                 forM_ entries $ \(key, value) -> do                     existing <- Map.lookup key cache@@ -130,5 +130,5 @@                             -- No in-flight computation: create fresh TMVar with value                             tmvar <- STM.newTMVar (Right value)                             Map.insert tmvar key cache-        RemoveFromCache keys -> do+        Remove keys -> do             STM.atomically $ forM_ keys $ \key -> Map.delete key cache
src/Atelier/Effects/Chan.hs view
@@ -26,11 +26,12 @@ module Atelier.Effects.Chan     ( -- * Effect       Chan-    , newChan-    , readChan-    , writeChan-    , dupChan-    , runChan+    , new+    , read+    , write+    , dup+    , run+    , readBatched        -- * Channel Types @@ -39,7 +40,6 @@       -- to maintain abstraction boundaries.     , InChan     , OutChan-    , readChanBatched     ) where @@ -64,33 +64,33 @@   -- | Run the 'Chan' effect, allowing channel operations to perform their IO.-runChan :: forall a es. (IOE :> es) => Eff (Chan : es) a -> Eff es a-runChan = evalStaticRep Chan+run :: forall a es. (IOE :> es) => Eff (Chan : es) a -> Eff es a+run = evalStaticRep Chan   -- | Create a new channel, returning its write ('InChan') and read ('OutChan') -- ends.-newChan :: forall a es. (Chan :> es) => Eff es (InChan a, OutChan a)-newChan =+new :: forall a es. (Chan :> es) => Eff es (InChan a, OutChan a)+new =     unsafeEff_ Unagi.newChan   -- | Read the next item from a channel, blocking until one is available.-readChan :: forall a es. (Chan :> es) => OutChan a -> Eff es a-readChan outChan =+read :: forall a es. (Chan :> es) => OutChan a -> Eff es a+read outChan =     unsafeEff_ $ Unagi.readChan outChan   -- | Write an item to a channel. Never blocks (the channel is unbounded).-writeChan :: forall a es. (Chan :> es) => InChan a -> a -> Eff es ()-writeChan inChan val =+write :: forall a es. (Chan :> es) => InChan a -> a -> Eff es ()+write inChan val =     unsafeEff_ $ Unagi.writeChan inChan val   -- | Duplicate a channel, producing a new read end that observes every item -- written after the duplication.-dupChan :: forall a es. (Chan :> es) => InChan a -> Eff es (OutChan a)-dupChan inChan =+dup :: forall a es. (Chan :> es) => InChan a -> Eff es (OutChan a)+dup inChan =     unsafeEff_ $ Unagi.dupChan inChan  @@ -99,7 +99,7 @@ -- Blocks until at least one item is available, then attempts to read -- up to @batchSize@ items total within the given timeout. Always returns -- at least one item.-readChanBatched+readBatched     :: forall t a es      . ( Chan :> es        , TimeUnit t@@ -113,13 +113,13 @@     -- ^ Channel to read from     -> Eff es (NonEmpty a)     -- ^ Non-empty batch of items (at least one, up to batch size)-readChanBatched timeoutDuration batchSize outChan = do+readBatched timeoutDuration batchSize outChan = do     evalState @[a] [] $ do-        h <- readChan outChan -- blocking, get first item+        h <- read outChan -- blocking, get first item         _ <- timeout (fromIntegral (toMicroseconds timeoutDuration))             $ replicateM_ (batchSize - 1)             $ do-                x <- readChan outChan+                x <- read outChan                 modify (x :)         rest <- get         pure $ h :| reverse rest
src/Atelier/Effects/Clock.hs view
@@ -9,10 +9,10 @@     , TimeZone     , currentTime     , currentTimeZone-    , runClock-    , runClockConst-    , runClockState-    , runClockList+    , runIO+    , runConst+    , runState+    , runList     ) where @@ -36,24 +36,24 @@   -- | Interpret 'Clock' against the real system clock.-runClock :: (IOE :> es) => Eff (Clock : es) a -> Eff es a-runClock = interpret_ $ \case+runIO :: (IOE :> es) => Eff (Clock : es) a -> Eff es a+runIO = interpret_ $ \case     CurrentTime -> liftIO getCurrentTime     CurrentTimeZone -> liftIO getCurrentTimeZone   -- | Interpret 'Clock' so 'currentTime' always returns a fixed time. The time -- zone is reported as 'utc'.-runClockConst :: UTCTime -> Eff (Clock : es) a -> Eff es a-runClockConst time = interpret_ $ \case+runConst :: UTCTime -> Eff (Clock : es) a -> Eff es a+runConst time = interpret_ $ \case     CurrentTime -> pure time     CurrentTimeZone -> pure utc   -- | Interpret 'Clock' so 'currentTime' reads from a mutable 'State' cell, -- letting a test advance \"now\" explicitly. The time zone is reported as 'utc'.-runClockState :: (State UTCTime :> es) => Eff (Clock : es) a -> Eff es a-runClockState = interpret_ $ \case+runState :: (State UTCTime :> es) => Eff (Clock : es) a -> Eff es a+runState = interpret_ $ \case     CurrentTime -> get     CurrentTimeZone -> pure utc @@ -61,8 +61,8 @@ -- | Scripted interpreter for testing: returns times from a pre-loaded list in -- order. Each 'currentTime' call pops the next item. Crashes if the list is -- exhausted.-runClockList :: [UTCTime] -> Eff (Clock : es) a -> Eff es a-runClockList times = reinterpret (evalState times) $ \_ -> \case+runList :: [UTCTime] -> Eff (Clock : es) a -> Eff es a+runList times = reinterpret (evalState times) $ \_ -> \case     CurrentTime ->         get >>= \case             [] -> error "runClockList: no more times in queue"
src/Atelier/Effects/Conc.hs view
@@ -3,8 +3,7 @@ -- 'Conc' exposes forking, awaiting, racing and nurseries ('scoped') as an -- effect, so concurrent code stays in 'Eff' and inherits structured-concurrency -- guarantees: every child thread is bound to a scope and is cancelled when that--- scope closes. The base interpreter ('runConc') ignores tracing; use--- "Atelier.Effects.Conc.Traced" to propagate OpenTelemetry context across forks.+-- scope closes. module Atelier.Effects.Conc     ( -- * Effect       Conc (..)@@ -24,11 +23,11 @@     , restartableForkLoop        -- * Interpreters-    , runConcBase-    , runConc+    , runScope+    , runIO        -- * Unlift Strategy-    , concStrat+    , strategy     ) where @@ -134,30 +133,27 @@   -- | Base interpreter: resolves 'Conc' operations using Ki.------ Does not handle trace context propagation. Use 'Atelier.Effects.Conc.Traced.runConc'--- for automatic span link propagation across forks.-runConcBase :: forall es a. (Concurrent :> es, IOE :> es) => Scope -> Eff (Conc : es) a -> Eff es a-runConcBase (Scope scope0) = interpret $ handler @es scope0+runScope :: forall es a. (Concurrent :> es, IOE :> es) => Scope -> Eff (Conc : es) a -> Eff es a+runScope (Scope scope0) = interpret $ handler @es scope0   where     handler :: forall es'. (Concurrent :> es', IOE :> es') => Ki.Scope -> EffectHandler Conc es'     handler scope env = \case         Fork action ->-            localUnliftIO env concStrat \unlift ->+            localUnliftIO env strategy \unlift ->                 fmap Thread . liftIO . Ki.fork scope $ unlift action         Fork_ action ->-            localUnliftIO env concStrat \unlift ->+            localUnliftIO env strategy \unlift ->                 Ki.fork_ scope $ unlift action         ForkTry action ->-            localUnliftIO env concStrat \unlift ->+            localUnliftIO env strategy \unlift ->                 fmap Thread . liftIO . Ki.forkTry scope $ unlift action         Await (Thread thread) ->             runConcurrent . atomically $ Ki.await thread         AwaitAll ->             runConcurrent . atomically $ Ki.awaitAll scope         Race ma mb ->-            localLend @'[Concurrent] env concStrat \lend ->-                localUnliftIO env concStrat \unlift ->+            localLend @'[Concurrent] env strategy \lend ->+                localUnliftIO env strategy \unlift ->                     Ki.scoped \raceScope -> do                         ref <- unlift $ inject $ lend newEmptyMVar                         void@@ -170,9 +166,9 @@                             $ lend . putMVar ref . Right =<< mb                         unlift $ lend $ takeMVar ref         Scoped m ->-            localUnlift env concStrat \unliftEff ->-                localLend @'[IOE, Concurrent] env concStrat \lend ->-                    withEffToIO concStrat \unliftIO ->+            localUnlift env strategy \unliftEff ->+                localLend @'[IOE, Concurrent] env strategy \lend ->+                    withEffToIO strategy \unliftIO ->                         Ki.scoped \subScope ->                             unliftIO                                 . unliftEff@@ -185,14 +181,14 @@ -- | Run 'Conc' in a new Ki scope without trace context propagation. -- -- Suitable for tests and contexts where tracing is not needed.-runConc :: (Concurrent :> es, IOE :> es) => Eff (Conc : es) a -> Eff es a-runConc eff = withEffToIO concStrat $ \unlift ->+runIO :: (Concurrent :> es, IOE :> es) => Eff (Conc : es) a -> Eff es a+runIO eff = withEffToIO strategy $ \unlift ->     Ki.scoped $ \scope ->-        unlift $ runConcBase (Scope scope) eff+        unlift $ runScope (Scope scope) eff   -- | The unlift strategy used when running forked actions: a persistent, -- unlimited concurrent unlift, so forked threads may themselves use the effects--- of the enclosing computation. Shared with "Atelier.Effects.Conc.Traced".-concStrat :: UnliftStrategy-concStrat = ConcUnlift Persistent Unlimited+-- of the enclosing computation.+strategy :: UnliftStrategy+strategy = ConcUnlift Persistent Unlimited
src/Atelier/Effects/Console.hs view
@@ -13,10 +13,10 @@     , putText        -- * Interpreters-    , runConsoleHandle-    , runConsole-    , runConsoleToList-    , runConsoleOut+    , runHandle+    , runIO+    , runToList+    , runOut     ) where @@ -58,24 +58,24 @@   -- | Interpret 'Console' by writing to the given 'Handle'.-runConsoleHandle :: (IOE :> es) => Handle -> Eff (Console : es) a -> Eff es a-runConsoleHandle h = interpret_ \case+runHandle :: (IOE :> es) => Handle -> Eff (Console : es) a -> Eff es a+runHandle h = interpret_ \case     PutStr s -> liftIO $ B8.hPut h s   -- | Interpret 'Console' by writing to 'stdout'.-runConsole :: (IOE :> es) => Eff (Console : es) a -> Eff es a-runConsole = runConsoleHandle stdout+runIO :: (IOE :> es) => Eff (Console : es) a -> Eff es a+runIO = runHandle stdout   -- | Interpret 'Console' by collecting all writes into a list, in order. Useful -- for asserting on output in tests.-runConsoleToList :: Eff (Console : es) a -> Eff es (a, [ByteString])-runConsoleToList = reinterpret_ (fmap (second reverse) . runState []) \case+runToList :: Eff (Console : es) a -> Eff es (a, [ByteString])+runToList = reinterpret_ (fmap (second reverse) . runState []) \case     PutStr s -> modify (s :)  -runConsoleOut :: (IOE :> es) => Output -> Eff (Console : es) a -> Eff es a-runConsoleOut = \case-    Stderr -> runConsoleHandle stderr-    Stdout -> runConsoleHandle stdout+runOut :: (IOE :> es) => Output -> Eff (Console : es) a -> Eff es a+runOut = \case+    Stderr -> runHandle stderr+    Stdout -> runHandle stdout
src/Atelier/Effects/Debounce.hs view
@@ -2,11 +2,11 @@ -- -- == Overview ----- 'debouncedWith' is the fundamental operation: it accumulates rapid successive+-- 'with' is the fundamental operation: it accumulates rapid successive -- calls with the same key using a merge function, then fires the callback once -- after the settle window with the merged result. ----- 'debounced' is a convenience wrapper for the common case where only timing+-- 'debounce' is a convenience wrapper for the common case where only timing -- matters and the last-registered action should fire. -- -- == Example@@ -14,25 +14,25 @@ -- @ -- -- Coalesce rapid saves per file: only the last edit within a 200ms window -- -- triggers a rebuild, keyed by path.--- onEdit :: (Debounce FilePath :> es) => FilePath -> Eff es ()+-- onEdit :: (Debouncing FilePath :> es) => FilePath -> Eff es () -- onEdit path = debounced 200 path (rebuild path) -- -- -- Or merge the burst instead of keeping just the last value. Here every -- -- event seen within the window is combined (list append) before a single -- -- rebuild fires with the full batch.--- onChange :: (Debounce FilePath :> es) => FilePath -> FileEvent -> Eff es ()+-- onChange :: (Debouncing FilePath :> es) => FilePath -> FileEvent -> Eff es () -- onChange path event =---     debouncedWith 200 (<>) path [event] (rebuildWith path)+--     with 200 (<>) path [event] (rebuildWith path) -- @ module Atelier.Effects.Debounce     ( -- * Effect       Debounce-    , debouncedWith-    , debounced+    , with+    , withArg        -- * Interpreters-    , runDebounce-    , runDebounceNoOp+    , run+    , runNoOp        -- * Helpers     , ensureEntry@@ -42,11 +42,17 @@ where  import Data.Dynamic (Dynamic, fromDynamic, toDyn)-import Effectful (Effect, Limit (..), Persistence (..), UnliftStrategy (..))+import Effectful+    ( Dispatch (..)+    , DispatchOf+    , Effect+    , Limit (..)+    , Persistence (..)+    , UnliftStrategy (..)+    ) import Effectful.Concurrent (Concurrent) import Effectful.Concurrent.STM (STM)-import Effectful.Dispatch.Dynamic (interpretWith, localSeqUnlift, localUnlift)-import Effectful.TH (makeEffect)+import Effectful.Dispatch.Dynamic (interpretWith, localSeqUnlift, localUnlift, send)  import Effectful.Concurrent.STM qualified as STM import StmContainers.Map qualified as Map@@ -62,11 +68,7 @@ -- | Effect for coalescing rapid bursts of keyed events into a single delayed -- callback. data Debounce key :: Effect where-    -- | Schedule @callback merged@ after the settle window. If another call-    -- with the same @key@ arrives before the window expires, the two values are-    -- combined with @merge old new@ and the timer resets. The callback fires at-    -- most once per burst.-    DebouncedWith+    WithArg         :: (Typeable arg)         => Millisecond         -> (arg -> arg -> arg)@@ -76,13 +78,23 @@         -> Debounce key m ()  -makeEffect ''Debounce+type instance DispatchOf (Debounce _) = 'Dynamic  +-- | Schedule @callback merged@ after the settle window. If another call+-- with the same @key@ arrives before the window expires, the two values are+-- combined with @merge old new@ and the timer resets. The callback fires at+-- most once per burst.+withArg+    :: (Debounce key :> es, Typeable arg)+    => Millisecond -> (arg -> arg -> arg) -> key -> arg -> (arg -> Eff es a) -> Eff es ()+withArg settleWindow merge key arg callback = send $ WithArg settleWindow merge key arg callback++ -- | Schedule @action@ to run after the settle window unless a newer call with -- the same @key@ arrives first. The last-registered action fires.-debounced :: (Debounce key :> es) => Millisecond -> key -> Eff es a -> Eff es ()-debounced settleMs key action = debouncedWith settleMs (\_ _ -> ()) key () (\_ -> action)+with :: (Debounce key :> es) => Millisecond -> key -> Eff es a -> Eff es ()+with settleMs key action = withArg settleMs (\_ _ -> ()) key () (\_ -> action)   -- | Per-key debounce state: the latest (merged) pending argument and a@@ -100,7 +112,7 @@ -- -- Each key maintains a generation counter. Rapid calls for the same key merge -- their values and reset the settle timer; only the last generation fires.-runDebounce+run     :: forall key es a      . ( Conc :> es        , Concurrent :> es@@ -109,10 +121,10 @@        )     => Eff (Debounce key : es) a     -> Eff es a-runDebounce eff = do+run eff = do     state <- STM.atomically (Map.new @key @Entry)     interpretWith eff \env -> \case-        DebouncedWith settleMs merge key arg callback -> do+        WithArg settleMs merge key arg callback -> do             entry <- STM.atomically $ ensureEntry key arg state merge             localUnlift env (ConcUnlift Persistent (Limited 1)) \unlift ->                 void@@ -177,6 +189,6 @@  -- | No-op interpreter — fires every callback immediately with its value, without -- debouncing. Useful for tests where timing is controlled externally.-runDebounceNoOp :: Eff (Debounce key : es) a -> Eff es a-runDebounceNoOp eff = interpretWith eff \env -> \case-    DebouncedWith _ _ _ value callback -> localSeqUnlift env \unlift -> void $ unlift (callback value)+runNoOp :: Eff (Debounce key : es) a -> Eff es a+runNoOp eff = interpretWith eff \env -> \case+    WithArg _ _ _ value callback -> localSeqUnlift env \unlift -> void $ unlift (callback value)
src/Atelier/Effects/Delay.hs view
@@ -1,23 +1,9 @@--- | An effect for delaying execution.------ 'wait' suspends the current thread for a 'TimeUnit' duration; 'every' repeats--- an action forever with a fixed gap between runs. 'runDelay' performs real--- delays, while 'runDelayNoOp' skips them so time-based code runs instantly--- under test.------ @--- -- pause within a workflow:--- wait (250 :: Millisecond)------ -- run a health check every 30 seconds, forever (the caller forks it):--- fork_ $ every (30 :: Second) checkHealth--- @ module Atelier.Effects.Delay     ( Delay     , wait     , every-    , runDelay-    , runDelayNoOp+    , runIO+    , runNoOp     ) where @@ -38,8 +24,7 @@   -- | Runs an action repeatedly, waiting the given duration in between each--- execution, starting immediately. Returns Void since it runs forever.--- Caller is responsible for forking.+-- execution, starting immediately. every :: (Delay :> es, TimeUnit t) => t -> Eff es () -> Eff es Void every delay action = forever do     action@@ -47,11 +32,11 @@   -- | Interpret 'Delay' using real thread delays.-runDelay :: (Concurrent :> es) => Eff (Delay : es) a -> Eff es a-runDelay = interpret_ \(Wait delay) ->+runIO :: (Concurrent :> es) => Eff (Delay : es) a -> Eff es a+runIO = interpret_ \(Wait delay) ->     threadDelay $ fromIntegral (toMicroseconds delay)  --- | Delay interpreter that makes every 'wait' a no-op — useful in unit tests.-runDelayNoOp :: Eff (Delay : es) a -> Eff es a-runDelayNoOp = interpret_ \(Wait _) -> pure ()+-- | Delay interpreter that makes every 'wait' return immediately.+runNoOp :: Eff (Delay : es) a -> Eff es a+runNoOp = interpret_ \(Wait _) -> pure ()
src/Atelier/Effects/Env.hs view
@@ -1,19 +1,17 @@ -- | An effect for reading process environment variables.------ 'runEnv' reads the real process environment; 'runEnvConst' supplies a fixed--- association list for tests. module Atelier.Effects.Env     ( Env-    , getEnvironment-    , lookupEnv-    , runEnv-    , runEnvConst+    , all+    , lookup+    , runIO+    , runConst     ) where  import Effectful (Effect, IOE) import Effectful.Dispatch.Dynamic (interpret_) import Effectful.TH (makeEffect)+import Prelude hiding (All (..), all)  import Data.List qualified as List import System.Environment qualified as System@@ -22,23 +20,23 @@ -- | Effect for reading the process environment. data Env :: Effect where     -- | The full environment as a list of @(name, value)@ pairs.-    GetEnvironment :: Env m [(String, String)]+    All :: Env m [(String, String)]     -- | Pick out one value from the environment.-    LookupEnv :: String -> Env m (Maybe String)+    Lookup :: String -> Env m (Maybe String)   makeEffect ''Env   -- | Interpret 'Env' against the real process environment.-runEnv :: (IOE :> es) => Eff (Env : es) a -> Eff es a-runEnv = interpret_ $ \case-    GetEnvironment -> liftIO System.getEnvironment-    LookupEnv key -> liftIO $ System.lookupEnv key+runIO :: (IOE :> es) => Eff (Env : es) a -> Eff es a+runIO = interpret_ $ \case+    All -> liftIO System.getEnvironment+    Lookup key -> liftIO $ System.lookupEnv key  --- | Interpret 'Env' with a fixed environment, for tests.-runEnvConst :: [(String, String)] -> Eff (Env : es) a -> Eff es a-runEnvConst env = interpret_ $ \case-    GetEnvironment -> pure env-    LookupEnv key -> pure $ List.lookup key env+-- | Interpret 'Env' with a fixed environment.+runConst :: [(String, String)] -> Eff (Env : es) a -> Eff es a+runConst env = interpret_ $ \case+    All -> pure env+    Lookup key -> pure $ List.lookup key env
src/Atelier/Effects/Exit.hs view
@@ -1,17 +1,14 @@ -- | An effect for terminating the program with an exit code.------ 'runExit' really exits the process; 'runExitNoOp' captures the exit code--- instead, so tests can assert on it without killing the test runner. module Atelier.Effects.Exit     ( -- * Effect       Exit-    , exitWith-    , exitSuccess-    , exitFailure+    , with+    , success+    , failure        -- * Interpreters-    , runExit-    , runExitNoOp+    , runIO+    , runNoOp     ) where @@ -27,30 +24,29 @@ -- | Effect for terminating the program with an exit code. data Exit :: Effect where     -- | Terminate the program with the given 'ExitCode'. Does not return.-    ExitWith :: ExitCode -> Exit m a+    With :: ExitCode -> Exit m a   makeEffect ''Exit   -- | Exit the program successfully (exit code 0).-exitSuccess :: (Exit :> es) => Eff es a-exitSuccess = exitWith ExitSuccess+success :: (Exit :> es) => Eff es a+success = with ExitSuccess   -- | Exit the program with a generic failure (exit code 1).-exitFailure :: (Exit :> es) => Eff es a-exitFailure = exitWith (ExitFailure 1)+failure :: (Exit :> es) => Eff es a+failure = with (ExitFailure 1)   -- | Interpret 'Exit' by actually terminating the process.-runExit :: (IOE :> es) => Eff (Exit : es) a -> Eff es a-runExit = interpret_ \(ExitWith code) -> liftIO (IO.exitWith code)+runIO :: (IOE :> es) => Eff (Exit : es) a -> Eff es a+runIO = interpret_ \(With code) -> liftIO (IO.exitWith code)   -- | Exit interpreter that captures exit calls instead of terminating the process. -- Returns 'Left' the exit code if the action exited, 'Right' the result otherwise.--- Useful in unit tests.-runExitNoOp :: Eff (Exit : es) a -> Eff es (Either ExitCode a)-runExitNoOp =-    reinterpret_ (runErrorNoCallStack @ExitCode) \(ExitWith code) -> throwError code+runNoOp :: Eff (Exit : es) a -> Eff es (Either ExitCode a)+runNoOp =+    reinterpret_ (runErrorNoCallStack @ExitCode) \(With code) -> throwError code
+ src/Atelier/Effects/FSNotify.hs view
@@ -0,0 +1,113 @@+{-# OPTIONS_GHC -Wno-redundant-constraints #-}++module Atelier.Effects.FSNotify+    ( FSNotify+    , withManager+    , startManager+    , stopManager+    , withManagerConf+    , startManagerConf+    , watchDir+    , watchDirChan+    , watchTree+    , watchTreeChan+    , module System.FSNotify+    , run+    )+where++import Effectful (Dispatch (..), DispatchOf, Effect, IOE, Limit (..), Persistence (..))+import Effectful.Dispatch.Static+    ( SideEffects (..)+    , StaticRep+    , concUnliftIO+    , evalStaticRep+    , unsafeEff+    , unsafeEff_+    )+import System.FSNotify hiding+    ( Action+    , startManager+    , startManagerConf+    , stopManager+    , watchDir+    , watchDirChan+    , watchTree+    , watchTreeChan+    , withManager+    , withManagerConf+    )++import System.FSNotify qualified as FSNotify+++data FSNotify :: Effect+++type instance DispatchOf FSNotify = Static 'WithSideEffects+++data instance StaticRep FSNotify = FSNotify+++type Action es = Event -> Eff es ()+++withManager :: (FSNotify :> es) => (WatchManager -> Eff es a) -> Eff es a+withManager act =+    unsafeEff \env ->+        concUnliftIO env Persistent Unlimited \unlift ->+            FSNotify.withManager (unlift . act)+++startManager :: (FSNotify :> es) => Eff es WatchManager+startManager = unsafeEff_ $ FSNotify.startManager+++stopManager :: (FSNotify :> es) => WatchManager -> Eff es ()+stopManager = unsafeEff_ . FSNotify.stopManager+++withManagerConf :: (FSNotify :> es) => WatchConfig -> (WatchManager -> Eff es a) -> Eff es a+withManagerConf conf act =+    unsafeEff \env ->+        concUnliftIO env Persistent Unlimited \unlift ->+            FSNotify.withManagerConf conf (unlift . act)+++startManagerConf :: (FSNotify :> es) => WatchConfig -> Eff es WatchManager+startManagerConf = unsafeEff_ . FSNotify.startManagerConf+++watchDir+    :: (FSNotify :> es) => WatchManager -> FilePath -> ActionPredicate -> Action es -> Eff es StopListening+watchDir watchManager filePath actionPredicate action =+    unsafeEff \env ->+        concUnliftIO env Persistent Unlimited \unlift ->+            FSNotify.watchDir watchManager filePath actionPredicate (unlift . action)+++watchDirChan+    :: (FSNotify :> es)+    => WatchManager -> FilePath -> ActionPredicate -> EventChannel -> Eff es StopListening+watchDirChan watchManager filePath actionPredicate eventChannel =+    unsafeEff_ $ FSNotify.watchDirChan watchManager filePath actionPredicate eventChannel+++watchTree+    :: (FSNotify :> es) => WatchManager -> FilePath -> ActionPredicate -> Action es -> Eff es StopListening+watchTree watchManager filePath actionPredicate action =+    unsafeEff \env ->+        concUnliftIO env Persistent Unlimited \unlift ->+            FSNotify.watchTree watchManager filePath actionPredicate (unlift . action)+++watchTreeChan+    :: (FSNotify :> es)+    => WatchManager -> FilePath -> ActionPredicate -> EventChannel -> Eff es StopListening+watchTreeChan watchManager filePath actionPredicate eventChannel =+    unsafeEff_ $ FSNotify.watchTreeChan watchManager filePath actionPredicate eventChannel+++run :: (IOE :> es) => Eff (FSNotify : es) a -> Eff es a+run = evalStaticRep FSNotify
+ src/Atelier/Effects/FSNotify/Devel.hs view
@@ -0,0 +1,33 @@+{-# OPTIONS_GHC -Wno-redundant-constraints #-}++module Atelier.Effects.FSNotify.Devel+    ( treeExtAny+    , treeExtExists+    , module System.FSNotify.Devel+    )+where++import Effectful (Limit (..), Persistence (..))+import Effectful.Dispatch.Static (concUnliftIO, unsafeEff)+import System.FSNotify (StopListening, WatchManager)+import System.FSNotify.Devel hiding (treeExtAny, treeExtExists)++import System.FSNotify.Devel qualified as FSNotify.Devel++import Atelier.Effects.FSNotify (FSNotify)+++treeExtAny+    :: (FSNotify :> es)+    => WatchManager -> FilePath -> Text -> (FilePath -> Eff es ()) -> Eff es StopListening+treeExtAny manager path extension action = unsafeEff \env ->+    concUnliftIO env Persistent Unlimited \unlift ->+        FSNotify.Devel.treeExtAny manager path extension (\fp -> unlift $ action fp)+++treeExtExists+    :: (FSNotify :> es)+    => WatchManager -> FilePath -> Text -> (FilePath -> Eff es ()) -> Eff es StopListening+treeExtExists manager path extension action = unsafeEff \env ->+    concUnliftIO env Persistent Unlimited \unlift ->+        FSNotify.Devel.treeExtExists manager path extension (\fp -> unlift $ action fp)
src/Atelier/Effects/File.hs view
@@ -1,17 +1,12 @@ {-# OPTIONS_GHC -Wno-redundant-constraints #-}  -- | A handle-based file IO effect.------ A thin 'Eff' wrapper over the handle operations in "System.IO" and--- "Data.Text.IO" ('withFile', 'hPutText', 'hGetLine', …), so file IO passes--- through the effect system instead of raw 'IO'. 'Handle' and 'BufferMode' are--- re-exported for convenience. module Atelier.Effects.File     ( File     , Handle     , BufferMode (..)     , IOMode (..)-    , withFile+    , with     , hClose     , hFlush     , hGetLine@@ -21,7 +16,7 @@     , hPutLBsLn     , hIsEOF     , hSetBuffering-    , runFile+    , run     ) where @@ -52,8 +47,8 @@   -- | Lifted `System.IO.withFile`:-withFile :: (File :> es, HasCallStack) => FilePath -> IOMode -> (Handle -> Eff es a) -> Eff es a-withFile fp m f = unsafeSeqUnliftIO \unlift -> IO.withFile fp m (unlift . f)+with :: (File :> es, HasCallStack) => FilePath -> IOMode -> (Handle -> Eff es a) -> Eff es a+with fp m f = unsafeSeqUnliftIO \unlift -> IO.withFile fp m (unlift . f)   -- | Lifted `System.IO.hClose`.@@ -103,5 +98,5 @@   -- | Run file operations.-runFile :: (HasCallStack, IOE :> es) => Eff (File : es) a -> Eff es a-runFile = evalStaticRep File+run :: (HasCallStack, IOE :> es) => Eff (File : es) a -> Eff es a+run = evalStaticRep File
src/Atelier/Effects/FileSystem.hs view
@@ -22,9 +22,10 @@     , getModificationTime     , getCurrentDirectory     , getXdgRuntimeDir-    , runFileSystemIO-    , runFileSystemNoOp-    , runFileSystemState+    , makeAbsolute+    , runIO+    , runNoOp+    , runState     ) where @@ -83,6 +84,16 @@     GetXdgRuntimeDir :: FileSystem m FilePath     -- | Obtain the time at which the file or directory was last modified.     GetModificationTime :: FilePath -> FileSystem m UTCTime+    -- | Convert a path into an absolute path. If the given path is relative,+    -- the current directory is prepended and then the combined result is+    -- normalized. If the path is already absolute, the path is simply+    -- normalized. The function preserves the presence or absence of the+    -- trailing path separator unless the path refers to the root directory /.+    --+    -- If the path is already absolute, the operation never fails. Otherwise,+    -- the operation may fail with the same exceptions as+    -- 'getCurrentDirectory'.+    MakeAbsolute :: FilePath -> FileSystem m FilePath   makeEffect ''FileSystem@@ -110,8 +121,8 @@   -- | Interpret 'FileSystem' against the real filesystem.-runFileSystemIO :: (IOE :> es) => Eff (FileSystem : es) a -> Eff es a-runFileSystemIO = interpret_ $ \case+runIO :: (IOE :> es) => Eff (FileSystem : es) a -> Eff es a+runIO = interpret_ $ \case     ReadFileBs path -> liftIO $ BS.readFile path     ReadFileLbsFrom path offset ->         liftIO@@ -131,12 +142,13 @@     GetCurrentDirectory -> liftIO Dir.getCurrentDirectory     GetXdgRuntimeDir -> liftIO $ fromMaybe "/tmp" <$> lookupEnv "XDG_RUNTIME_DIR"     GetModificationTime path -> liftIO $ Dir.getModificationTime path+    MakeAbsolute path -> liftIO $ Dir.makeAbsolute path   -- | Interpret 'FileSystem' with inert results: reads return empty, existence -- checks return 'False', and mutations do nothing. Useful for tests.-runFileSystemNoOp :: Eff (FileSystem : es) a -> Eff es a-runFileSystemNoOp = interpret_ $ \case+runNoOp :: Eff (FileSystem : es) a -> Eff es a+runNoOp = interpret_ $ \case     ReadFileBs _ -> pure mempty     ReadFileLbsFrom _ _ -> pure mempty     DoesFileExist _ -> pure False@@ -151,15 +163,16 @@     GetCurrentDirectory -> pure "."     GetXdgRuntimeDir -> pure "/tmp"     GetModificationTime _ -> pure $ UTCTime (Day.fromOrdinalDate 1970 1) 0+    MakeAbsolute path -> pure path   -- | Run `FileSystem` effect backed by a `State` effect with a `Map`. The keys -- of the `Map` are treated as file names. Only the `dirname` of files are -- counted as existing directories.-runFileSystemState+runState     :: (State (Map FilePath ByteString) :> es)     => Eff (FileSystem : es) a -> Eff es a-runFileSystemState = interpret_ \case+runState = interpret_ \case     ReadFileBs fp ->         maybe (throwIO $ userError "No such file") pure             =<< gets (M.lookup fp)@@ -183,3 +196,4 @@     GetCurrentDirectory -> pure "/"     GetXdgRuntimeDir -> pure "/tmp"     GetModificationTime _ -> pure $ UTCTime (Day.fromOrdinalDate 1970 1) 0+    MakeAbsolute path -> pure path
src/Atelier/Effects/FileWatcher.hs view
@@ -37,30 +37,14 @@ -- watchFilePathsDebounced (sourceWatches ++ cabalWatches) (markDirty . changeKindFor) -- @ module Atelier.Effects.FileWatcher-    ( -- * Watch specification-      Watch--      -- ** Constructors-    , dir-    , dirExt-    , dirWhere--      -- ** Combinators-    , excluding-    , containing--      -- * File events-    , FileEvent (..)-    , mergeFileEvent--      -- * Effect-    , FileWatcher+    ( -- * Effect+      FileWatcher     , watchFilePaths     , watchFilePathsDebounced        -- * Interpreters-    , runFileWatcherIO-    , runFileWatcherScripted+    , runIO+    , runScripted        -- * Internals (exported for testing)     , deduplicateDirs@@ -68,96 +52,34 @@     ) where -import Control.Concurrent (threadDelay) import Data.List (nub)-import Effectful (Effect, IOE)+import Effectful (Effect, Limit (..), Persistence (..), UnliftStrategy (..)) import Effectful.Concurrent (Concurrent)-import Effectful.Dispatch.Dynamic (interpretWith, localSeqUnlift, localUnliftIO, reinterpret)+import Effectful.Dispatch.Dynamic+    ( interpret+    , localSeqUnlift+    , localUnlift+    , reinterpret+    ) import Effectful.State.Static.Shared (evalState, get, put) import Effectful.TH (makeEffect)-import System.Directory (makeAbsolute)-import System.FSNotify (eventPath, watchTree, withManager)-import System.FilePath (takeExtension)+import System.FSNotify (eventPath) -import Data.Text qualified as T import Effectful.Concurrent qualified as Concurrent-import System.FSNotify qualified as FSN -import Atelier.Effects.Conc (concStrat)-import Atelier.Effects.Debounce (Debounce, debouncedWith)----- | The kind of filesystem change that triggered a watch callback.-data FileEvent-    = -- | A new file was created.-      Added-    | -- | An existing file was modified.-      Modified-    | -- | A file was deleted.-      Removed-    deriving stock (Eq, Show)----- | Merge two 'FileEvent's for the same path within a debounce window.------ Rules (old, new) → result:------ * @(_, Removed)@ → @Removed@: deletion always wins.--- * @(Removed, Added)@ → @Added@: explicit re-creation after deletion.--- * @(Added, _)@ → @Added@: a create-then-write is still a creation.--- * Otherwise → the newer event.-mergeFileEvent :: FileEvent -> FileEvent -> FileEvent-mergeFileEvent _ Removed = Removed-mergeFileEvent Removed Added = Added-mergeFileEvent Added _ = Added-mergeFileEvent _ event = event----- | Specification of a directory to watch recursively, with a file predicate.-data Watch = Watch FilePath (FilePath -> Bool)----- | Watch all files in a directory recursively.-dir :: FilePath -> Watch-dir p = Watch p (const True)----- | Watch files matching a predicate in a directory recursively.------ @--- dirWhere "src" (\f -> takeExtension f == ".hs")--- @-dirWhere :: FilePath -> (FilePath -> Bool) -> Watch-dirWhere = Watch----- | Watch files with a specific extension in a directory recursively.------ @--- dirExt "src" ".hs"--- @-dirExt :: FilePath -> Text -> Watch-dirExt p ext = dirWhere p (\f -> toText (takeExtension f) == ext)----- | Narrow a 'Watch' to exclude files matching a predicate.------ @--- dirExt "src" ".hs" \`excluding\` containing "dist-newstyle"--- @-excluding :: Watch -> (FilePath -> Bool) -> Watch-excluding (Watch p predicate) excl = Watch p (\f -> predicate f && not (excl f))-+import Atelier.Effects.Debounce (Debounce)+import Atelier.Effects.Delay (Delay)+import Atelier.Effects.FSNotify (FSNotify)+import Atelier.Effects.FileSystem (FileSystem)+import Atelier.Effects.FileWatcher.Event (Event)+import Atelier.Effects.FileWatcher.Watch (Watch) --- | Predicate: the file path contains the given fragment.------ Intended for use with 'excluding':------ @--- dir "src" \`excluding\` containing "dist-newstyle"--- @-containing :: Text -> FilePath -> Bool-containing fragment f = fragment `T.isInfixOf` toText f+import Atelier.Effects.Debounce qualified as Debounce+import Atelier.Effects.Delay qualified as Delay+import Atelier.Effects.FSNotify qualified as FSNotify+import Atelier.Effects.FileWatcher.Event qualified as Event+import Atelier.Effects.FileWatcher.Watch qualified as Watch+import Atelier.Time qualified as Time   data FileWatcher :: Effect where@@ -166,7 +88,7 @@     -- the minimal set of OS watchers needed to cover all entries (deduplication     -- by path prefix). The callback is invoked at most once per raw filesystem     -- event regardless of how many entries match. Never returns.-    WatchFilePaths :: [Watch] -> (FilePath -> FileEvent -> m a) -> FileWatcher m Void+    WatchFilePaths :: [Watch] -> (FilePath -> Event -> m a) -> FileWatcher m Void   makeEffect ''FileWatcher@@ -178,55 +100,49 @@ watchFilePathsDebounced     :: (Debounce FilePath :> es, FileWatcher :> es)     => [Watch]-    -> (FilePath -> FileEvent -> Eff es ())+    -> (FilePath -> Event -> Eff es ())     -> Eff es Void watchFilePathsDebounced watches callback =     watchFilePaths watches \path event ->-        debouncedWith 100 mergeFileEvent path event (callback path)+        Debounce.withArg 100 (<>) path event (callback path)   -- | Production interpreter backed by fsnotify.-runFileWatcherIO :: (IOE :> es) => Eff (FileWatcher : es) a -> Eff es a-runFileWatcherIO eff = interpretWith eff \env -> \case-    WatchFilePaths watches callback ->-        localUnliftIO env concStrat \unliftIO -> do-            absWatches <- forM watches \(Watch p predicate) ->-                (\absP -> Watch absP predicate) <$> makeAbsolute p-            withManager \mgr -> do-                let dedupedDirs = deduplicateDirs [p | Watch p _ <- absWatches]+runIO :: (Delay :> es, FSNotify :> es, FileSystem :> es) => Eff (FileWatcher : es) a -> Eff es a+runIO = interpret \env -> \case+    WatchFilePaths watches callback -> do+        absWatches <- forM watches Watch.makeAbsolute+        FSNotify.withManager \mgr -> do+            let dedupedDirs = deduplicateDirs $ Watch.path <$> absWatches+            localUnlift env (ConcUnlift Persistent Unlimited) \unlift ->                 for_ dedupedDirs \d ->-                    watchTree mgr d (matchesAny absWatches . eventPath) \fsEvent ->-                        void $ unliftIO $ callback (eventPath fsEvent) (toFileEvent fsEvent)-                -- Park indefinitely to hold the manager open. fsnotify delivers-                -- events on its own threads, so there is nothing to do here, and-                -- waking up is not free: each wake-up ends the RTS idle period-                -- and re-arms idle GC, costing a full major collection. 'forever'-                -- is belt-and-braces in case the delay ever returns early.-                ---                -- Preferred over @atomically retry@: a transaction that retries-                -- without having read a 'TVar' has no wakeup path, so it stays-                -- parked only for as long as GC still sees this thread as-                -- reachable. Should it ever become unreachable the RTS throws-                -- @BlockedIndefinitelyOnSTM@, which would unwind 'withManager'-                -- and silently stop all watching. A delay carries no such-                -- dependency on how callers happen to retain the thread.-                forever $ threadDelay maxBound+                    FSNotify.watchTree mgr d (matchesAny absWatches . eventPath) \fsEvent ->+                        void $ unlift $ callback (eventPath fsEvent) (Event.fromFS fsEvent)+            -- Park indefinitely to hold the manager open. fsnotify delivers+            -- events on its own threads, so there is nothing to do here, and+            -- waking up is not free: each wake-up ends the RTS idle period+            -- and re-arms idle GC, costing a full major collection. 'forever'+            -- is belt-and-braces in case the delay ever returns early.+            forever+                $ Delay.wait @Time.Microsecond+                $ Time.fromMicroseconds+                $ fromIntegral (maxBound :: Int)   -- | Scripted interpreter for testing. -- -- Delivers all scripted events to the callback in order, then parks forever,--- matching the blocking semantics of 'runFileWatcherIO' — 'WatchFilePaths'--- returns 'Void', so callers must never be resumed. Parks with a delay rather--- than @atomically retry@ for the same reason as 'runFileWatcherIO': a retry--- that read no 'TVar' stays parked only while GC sees the thread as reachable,--- so whether it survives depends on whether the test happens to hold onto the+-- matching the blocking semantics of 'runIO' — 'WatchFilePaths' returns+-- 'Void', so callers must never be resumed. Parks with a delay rather than+-- @atomically retry@ for the same reason as 'runIO': a retry that+-- read no 'TVar' stays parked only while GC sees the thread as reachable, so+-- whether it survives depends on whether the test happens to hold onto the -- 'ThreadId'. -- -- The 'Watch' specification is ignored; the caller controls what events are fed in.-runFileWatcherScripted-    :: (Concurrent :> es) => [(FilePath, FileEvent)] -> Eff (FileWatcher : es) a -> Eff es a-runFileWatcherScripted events = reinterpret (evalState events) \env -> \case+runScripted+    :: (Concurrent :> es) => [(FilePath, Event)] -> Eff (FileWatcher : es) a -> Eff es a+runScripted events = reinterpret (evalState events) \env -> \case     WatchFilePaths _ callback ->         localSeqUnlift env \unlift -> do             events' <- get@@ -237,32 +153,13 @@  -- Helpers -toFileEvent :: FSN.Event -> FileEvent-toFileEvent = \case-    FSN.Added {} -> Added-    FSN.Modified {} -> Modified-    FSN.ModifiedAttributes {} -> Modified-    FSN.CloseWrite {} -> Modified-    FSN.Removed {} -> Removed-    FSN.WatchedDirectoryRemoved {} -> Removed-    FSN.Unknown {} -> Modified-- -- | Remove dirs that are proper subdirectories of another dir in the list. deduplicateDirs :: [FilePath] -> [FilePath] deduplicateDirs dirs = filter notRedundant (nub dirs)   where-    notRedundant d = not $ any (\d' -> d' /= d && isStrictAncestor d' d) dirs----- | @isStrictAncestor parent child@ is true iff @parent@ is a proper ancestor--- of @child@ in the filesystem hierarchy.-isStrictAncestor :: FilePath -> FilePath -> Bool-isStrictAncestor parent child = (parent <> "/") `isPrefixOf` child+    notRedundant d = not $ any (\d' -> d' /= d && Watch.isStrictAncestor d' d) dirs   -- | Check whether a file path matches at least one 'Watch' entry. matchesAny :: [Watch] -> FilePath -> Bool-matchesAny watches path = any matches watches-  where-    matches (Watch d predicate) = (d == path || isStrictAncestor d path) && predicate path+matchesAny watches path = any (Watch.matches path) watches
+ src/Atelier/Effects/FileWatcher/Event.hs view
@@ -0,0 +1,49 @@+module Atelier.Effects.FileWatcher.Event+    ( Event (..)+    , merge+    , fromFS+    )+where++import System.FSNotify qualified as FSN+++-- | The kind of filesystem change that triggered a watch callback.+data Event+    = -- | A new file was created.+      Added+    | -- | An existing file was modified.+      Modified+    | -- | A file was deleted.+      Removed+    deriving stock (Eq, Show)+++instance Semigroup Event where+    (<>) = merge+++-- | Merge two 'Event's for the same path within a debounce window.+--+-- Rules (old, new) → result:+--+-- * @(_, Removed)@ → @Removed@: deletion always wins.+-- * @(Removed, Added)@ → @Added@: explicit re-creation after deletion.+-- * @(Added, _)@ → @Added@: a create-then-write is still a creation.+-- * Otherwise → the newer event.+merge :: Event -> Event -> Event+merge _ Removed = Removed+merge Removed Added = Added+merge Added _ = Added+merge _ event = event+++fromFS :: FSN.Event -> Event+fromFS = \case+    FSN.Added {} -> Added+    FSN.Modified {} -> Modified+    FSN.ModifiedAttributes {} -> Modified+    FSN.CloseWrite {} -> Modified+    FSN.Removed {} -> Removed+    FSN.WatchedDirectoryRemoved {} -> Removed+    FSN.Unknown {} -> Modified
+ src/Atelier/Effects/FileWatcher/Watch.hs view
@@ -0,0 +1,95 @@+module Atelier.Effects.FileWatcher.Watch+    ( -- * Watch specification+      Watch+    , path++      -- ** Constructors+    , dir+    , dirExt+    , dirWhere++      -- ** Combinators+    , excluding+    , containing++      -- ** Check+    , matches++      -- ** Utils+    , isStrictAncestor+    , makeAbsolute+    )+where++import Data.Text qualified as T+import System.FilePath qualified as Path++import Atelier.Effects.FileSystem (FileSystem)++import Atelier.Effects.FileSystem qualified as FileSystem+++-- | Specification of a directory to watch recursively, with a file predicate.+data Watch = Watch FilePath (FilePath -> Bool)+++path :: Watch -> FilePath+path (Watch p _) = p+++-- | Watch all files in a directory recursively.+dir :: FilePath -> Watch+dir p = Watch p (const True)+++-- | Watch files matching a predicate in a directory recursively.+--+-- @+-- dirWhere "src" (\f -> takeExtension f == ".hs")+-- @+dirWhere :: FilePath -> (FilePath -> Bool) -> Watch+dirWhere = Watch+++-- | Watch files with a specific extension in a directory recursively.+--+-- @+-- dirExt "src" ".hs"+-- @+dirExt :: FilePath -> Text -> Watch+dirExt p ext = dirWhere p (\f -> toText (Path.takeExtension f) == ext)+++-- | Narrow a 'Watch' to exclude files matching a predicate.+--+-- @+-- dirExt "src" ".hs" \`excluding\` containing "dist-newstyle"+-- @+excluding :: Watch -> (FilePath -> Bool) -> Watch+excluding (Watch p predicate) excl = Watch p (\f -> predicate f && not (excl f))+++-- | Predicate: the file path contains the given fragment.+--+-- Intended for use with 'excluding':+--+-- @+-- dir "src" \`excluding\` containing "dist-newstyle"+-- @+containing :: Text -> FilePath -> Bool+containing fragment f = fragment `T.isInfixOf` toText f+++matches :: FilePath -> Watch -> Bool+matches filePath (Watch d predicate) =+    (d == filePath || isStrictAncestor d filePath) && predicate filePath+++-- | @isStrictAncestor parent child@ is true iff @parent@ is a proper ancestor+-- of @child@ in the filesystem hierarchy.+isStrictAncestor :: FilePath -> FilePath -> Bool+isStrictAncestor parent child = (parent <> "/") `isPrefixOf` child+++makeAbsolute :: (FileSystem :> es) => Watch -> Eff es Watch+makeAbsolute (Watch p predicate) = flip Watch predicate <$> FileSystem.makeAbsolute p
+ src/Atelier/Effects/GenUUID.hs view
@@ -0,0 +1,38 @@+-- | An effect for generating UUIDs.+module Atelier.Effects.GenUUID+    ( GenUUID+    , UUID+    , gen+    , runIO+    , runConst+    )+where++import Data.UUID (UUID)+import Data.UUID.V4 (nextRandom)+import Effectful (Effect, IOE)+import Effectful.Dispatch.Dynamic (interpret_)+import Effectful.TH (makeEffect)+++-- | Effect for generating UUIDs.+data GenUUID :: Effect where+    -- | Generate a UUID.+    Gen :: GenUUID m UUID+++makeEffect ''GenUUID+++-- | Interpret 'GenUUID' by generating fresh random (version 4) UUIDs.+--+-- Produces fresh random (version 4) UUIDs.+runIO :: (IOE :> es) => Eff (GenUUID : es) a -> Eff es a+runIO = interpret_ \Gen -> liftIO nextRandom+++-- | Interpret 'GenUUID' so 'gen' always returns the given fixed UUID, for tests.+--+-- Returns a fixed UUID every time, for deterministic tests.+runConst :: UUID -> Eff (GenUUID : es) a -> Eff es a+runConst uuid = interpret_ \Gen -> pure uuid
src/Atelier/Effects/Input.hs view
@@ -1,8 +1,4 @@ -- | An effect for requesting a value from the environment on each call.------ Unlike 'Effectful.Reader.Static.Reader', 'Input' makes no assumption that the--- value is stable across the computation — each 'input' may observe a different--- value. See the 'Input' type below for the full comparison. module Atelier.Effects.Input     ( Input     , input@@ -10,8 +6,6 @@     , withConst     , toReader     , fromState-    , runInputEff-    , runInputConst     , map     ) where@@ -28,12 +22,11 @@  -- | Request a value from the effect system. ----- This effect does not carry any--- semantics as to how the value is created, whether it is cached, or whether--- you get a different value for each call. It is similar to--- 'Effectful.Reader.Static.Reader' in that it is about requesting a value, but--- 'Reader' carries the implicit assumption that its value remains static for--- the duration of the encapsulated computation, while 'Input' carries no such+-- This effect does not carry any semantics as to how the value is created,+-- whether it is cached, or whether you get a different value for each call. It+-- is similar to 'Reader' in that it is about requesting a value, but 'Reader'+-- carries the implicit assumption that its value remains static for the+-- duration of the encapsulated computation, while 'Input' carries no such -- assumption. -- -- If the value you wish to request from the effect system will remain@@ -47,12 +40,6 @@   -- | Runs the passed effectful action every time 'input' is invoked.-runInputEff :: Eff es i -> Eff (Input i : es) a -> Eff es a-runInputEff = withEff-{-# DEPRECATED runInputEff "Use 'Atelier.Effects.Input.withEff' instead." #-}----- | Runs the passed effectful action every time 'input' is invoked. withEff :: Eff es i -> Eff (Input i : es) a -> Eff es a withEff mk = interpret_ \Input -> mk @@ -60,16 +47,8 @@ -- | Provides the same value for every invocation of the 'input' operation. -- This is equivalent to using 'Effectful.Reader.Static' or -- 'Effectful.Reader.Local', but is available here for testing.-runInputConst :: i -> Eff (Input i : es) a -> Eff es a-runInputConst = withConst-{-# DEPRECATED runInputConst "Use 'Atelier.Effects.Input.withConst' instead." #-}----- | Provides the same value for every invocation of the 'input' operation.--- This is equivalent to using 'Effectful.Reader.Static' or--- 'Effectful.Reader.Local', but is available here for testing. withConst :: i -> Eff (Input i : es) a -> Eff es a-withConst = runInputEff . pure+withConst = withEff . pure   -- | Retrieves the value once, and exposes it as a 'Reader' effect.
src/Atelier/Effects/Internal/Coroutine.hs view
@@ -217,9 +217,9 @@     -> Eff (Await a : es) r     -> Eff es r awaitYield yields awaits = Conc.scoped do-    (inChan, outChan) <- Chan.newChan @a-    let yielder = interpretWith_ yields \(Yield x) -> Chan.writeChan inChan x-        awaiter = interpretWith_ awaits \Await -> Chan.readChan outChan+    (inChan, outChan) <- Chan.new @a+    let yielder = interpretWith_ yields \(Yield x) -> Chan.write inChan x+        awaiter = interpretWith_ awaits \Await -> Chan.read outChan     -- Ensure the awaiter runs first.     awaitThread <- Conc.fork awaiter     yieldThread <- Conc.fork yielder
src/Atelier/Effects/Iterator.hs view
@@ -47,9 +47,9 @@     => (Iterator es event -> Eff es a)     -> Eff es a fromEvents use = Conc.scoped do-    (inChan, outChan) <- Chan.newChan-    Sub.forkListener_ (Chan.writeChan inChan)-    use $ Iterator (Chan.readChan outChan)+    (inChan, outChan) <- Chan.new+    Sub.forkListener_ (Chan.write inChan)+    use $ Iterator (Chan.read outChan)   -- | Iterate only over values that pass a predicate.
src/Atelier/Effects/Log.hs view
@@ -37,10 +37,10 @@     , withNamespace        -- * Interpreters-    , runLog-    , runLogNoOp-    , runLogToHandle-    , runLogWriter+    , runIO+    , runNoOp+    , runToHandle+    , runWriter        -- * Utils     , minimumSeverityFromEnv@@ -49,9 +49,9 @@  import Data.Aeson (FromJSON (..), ToJSON) import Data.Default (Default (..))-import Data.List (lookup) import Effectful (Effect, IOE) import Effectful.Dispatch.Dynamic (localSeqUnlift, reinterpret, reinterpretWith)+import Effectful.NonDet (OnEmptyPolicy (..), emptyEff, runNonDet) import Effectful.Reader.Static (Reader, ask, local, runReader) import Effectful.TH (makeEffect) import Effectful.Writer.Static.Shared (Writer, tell)@@ -59,12 +59,14 @@  import Data.ByteString.Char8 qualified as B8 -import Atelier.Effects.Env (Env, getEnvironment)+import Atelier.Effects.Env (Env) import Atelier.Types.JSON.QuietSnake (QuietSnake (..)) import Atelier.Types.JSON.ReadShow (ReadShow (..))-import Atelier.Types.WithDefaults (WithDefaults (..))+import Atelier.Types.JSON.WithDefaults (WithDefaults (..)) +import Atelier.Effects.Env qualified as Env + -- | Effect for structured logging with hierarchical namespaces. data Log :: Effect where     -- | Emit a fully-formed log 'Message'.@@ -171,8 +173,8 @@   -- | Consumes `Log` effects, and discards the logged messages-runLogNoOp :: Eff (Log : es) a -> Eff es a-runLogNoOp = reinterpret (runReader (Namespace "")) $ \env -> \case+runNoOp :: Eff (Log : es) a -> Eff es a+runNoOp = reinterpret (runReader (Namespace "")) $ \env -> \case     LogMsg _ -> pure ()     WithNamespace ns act -> localSeqUnlift env $ \unlift ->         local (<> ns) $ unlift act@@ -184,8 +186,8 @@ -- The minimum severity defaults to 'Config'\'s @minimumSeverity@, but can be -- overridden at runtime by the @DEBUG@, @LOGGING@ or @LOG@ environment -- variables (checked in that order).-runLog :: (Env :> es, IOE :> es, Reader Config :> es) => Eff (Log : es) a -> Eff es a-runLog action = do+runIO :: (Env :> es, IOE :> es, Reader Config :> es) => Eff (Log : es) a -> Eff es a+runIO action = do     config <- ask     severity <- fromMaybe config.minimumSeverity <$> minimumSeverityFromEnv @@ -208,8 +210,8 @@  -- | Like 'runLog' but writes to the given 'Handle' instead of stdout. -- Useful for daemons that want to write logs to a file.-runLogToHandle :: (IOE :> es) => Handle -> Severity -> Eff (Log : es) a -> Eff es a-runLogToHandle handle minSeverity action =+runToHandle :: (IOE :> es) => Handle -> Severity -> Eff (Log : es) a -> Eff es a+runToHandle handle minSeverity action =     reinterpretWith (runReader (Namespace "")) action \env -> \case         LogMsg msg ->             liftIO $ when (msg.severity >= minSeverity) do@@ -221,8 +223,8 @@   -- | Interpret 'Log' by collecting messages into a 'Writer', for tests.-runLogWriter :: (Writer [Message] :> es) => Eff (Log : es) a -> Eff es a-runLogWriter = reinterpret (runReader (Namespace "")) $ \env -> \case+runWriter :: (Writer [Message] :> es) => Eff (Log : es) a -> Eff es a+runWriter = reinterpret (runReader (Namespace "")) $ \env -> \case     LogMsg msg -> tell [msg]     WithNamespace ns act -> localSeqUnlift env $ \unlift ->         local (<> ns) $ unlift act@@ -244,11 +246,22 @@  minimumSeverityFromEnv :: (Env :> es) => Eff es (Maybe Severity) minimumSeverityFromEnv = do-    env <- getEnvironment-    let overrideLog = (>>= readMaybe) $ lookup "LOG" env-        overrideLogging = (>>= readMaybe) $ lookup "LOGGING" env-        overrideDebug = (>>= \x -> if x == "0" then Nothing else Just DEBUG) $ lookup "DEBUG" env-    pure $ overrideDebug <|> overrideLogging <|> overrideLog+    fmap rightToMaybe+        $ runNonDet OnEmptyKeep+        $ asum+            [ Env.lookup "LOG" >>= readAlt+            , Env.lookup "LOGGING" >>= readAlt+            , Env.lookup "DEBUG" >>= \case+                Nothing -> emptyEff+                Just x+                    | x == "0" -> emptyEff+                    | otherwise -> pure DEBUG+            ]+  where+    readAlt Nothing = emptyEff+    readAlt (Just x) = case readMaybe x of+        Nothing -> emptyEff+        Just x' -> pure x'   square :: Text -> Text
src/Atelier/Effects/Posix/Daemons.hs view
@@ -1,54 +1,70 @@ -- | An effect for managing detached daemon processes via PID files. ----- 'daemonize' forks a program into the background, 'isRunning' checks a daemon's--- PID file, and 'forceKillAndWait' terminates one. Backed by the @daemons@--- package.+-- Backed by the @daemons@ package. module Atelier.Effects.Posix.Daemons     ( Daemons     , PidFile (..)     , daemonize     , isRunning     , forceKillAndWait-    , runDaemons+    , run     ) where  import Data.Default (def)-import Effectful (Effect, IOE, Limit (..), Persistence (..), UnliftStrategy (..))-import Effectful.Dispatch.Dynamic (interpretWith, localUnliftIO)+import Effectful (Dispatch (..), DispatchOf, Effect, IOE, Limit (..), Persistence (..))+import Effectful.Dispatch.Static+    ( SideEffects (..)+    , StaticRep+    , concUnliftIO+    , evalStaticRep+    , getStaticRep+    , unsafeEff+    , unsafeEff_+    ) import Effectful.Exception (trySync)-import Effectful.TH (makeEffect)  import System.Posix.Daemon qualified as Daemons   -- | Effect for managing detached daemon processes.-data Daemons :: Effect where-    -- | Daemonize the given program, ensuring it is cleanly separated from the-    -- spawning process.-    Daemonize :: PidFile -> m () -> Daemons m ()-    -- | Check whether a daemon is running for the provided PID file.-    IsRunning :: PidFile -> Daemons m Bool-    -- | Kill the daemon associated with the provided PID file using `SIGKILL`.-    ForceKillAndWait :: PidFile -> Daemons m (Either SomeException ())+data Daemons :: Effect  +type instance DispatchOf Daemons = Static 'WithSideEffects+++data instance StaticRep Daemons = Daemons++ -- | The path to a daemon's PID file, used to track and control it. newtype PidFile = PidFile {getPidFile :: FilePath}  -makeEffect ''Daemons+-- | Daemonize the given program, ensuring it is cleanly separated from the+-- spawning process.+daemonize :: (Daemons :> es) => PidFile -> Eff es () -> Eff es ()+daemonize (PidFile pidFile) program = do+    Daemons <- getStaticRep+    unsafeEff \env ->+        concUnliftIO env Persistent Unlimited \unlift -> do+            Daemons.runDetached (Just pidFile) def+                $ unlift program  --- | Interpret 'Daemons' using the @daemons@ package.-runDaemons :: (IOE :> es) => Eff (Daemons : es) a -> Eff es a-runDaemons act = do-    interpretWith act \env -> \case-        Daemonize (PidFile pidFile) program ->-            localUnliftIO env (ConcUnlift Persistent Unlimited) \unlift -> do-                Daemons.runDetached (Just pidFile) def-                    $ unlift program-        IsRunning (PidFile pidFile) ->-            liftIO $ Daemons.isRunning pidFile-        ForceKillAndWait (PidFile pidFile) ->-            trySync $ liftIO $ Daemons.brutalKill pidFile+-- | Check whether a daemon is running for the provided PID file.+isRunning :: (Daemons :> es) => PidFile -> Eff es Bool+isRunning (PidFile pidFile) = do+    Daemons <- getStaticRep+    unsafeEff_ $ Daemons.isRunning pidFile+++-- | Kill the daemon associated with the provided PID file using `SIGKILL`.+forceKillAndWait :: (Daemons :> es) => PidFile -> Eff es (Either SomeException ())+forceKillAndWait (PidFile pidFile) = do+    Daemons <- getStaticRep+    trySync $ unsafeEff_ $ Daemons.brutalKill pidFile+++run :: (IOE :> es) => Eff (Daemons : es) a -> Eff es a+run = evalStaticRep Daemons
src/Atelier/Effects/Process.hs view
@@ -1,3 +1,5 @@+{-# OPTIONS_GHC -Wno-redundant-constraints #-}+ -- | Effect for spawning and interacting with external processes. -- -- Unifies @typed-process@ (configuration, lifecycle and stream capture) and the@@ -38,15 +40,14 @@     , getExecutablePath        -- * Interpreters-    , runProcessIO+    , run     ) where  import Control.Exception (IOException, catch)-import Effectful (Effect, IOE)-import Effectful.Dispatch.Dynamic (interpret_)+import Effectful (Dispatch (..), DispatchOf, Effect, IOE)+import Effectful.Dispatch.Static (SideEffects (..), StaticRep, evalStaticRep, unsafeEff_) import Effectful.Exception (bracket, trySync)-import Effectful.TH (makeEffect) import System.Exit (ExitCode (..)) import System.Posix.Signals (sigTERM, signalProcessGroup) import System.Process (Pid, getPid, interruptProcessGroupOf)@@ -83,36 +84,15 @@ getStderr (RunningProcess p) = TP.getStderr p  -data Process :: Effect where-    -- | Run a process to completion, returning its exit code and captured stdout.-    ReadProcessStdout :: ProcessConfig i o e -> Process m (ExitCode, LByteString)-    -- | Run a process to completion, capturing both stdout and stderr along with-    -- its exit code.-    ReadProcess :: ProcessConfig i o e -> Process m (ExitCode, LByteString, LByteString)-    -- | Run a process to completion, returning only its exit code. Unlike-    -- 'ReadProcessStdout' this does not capture stdout.-    RunProcess :: ProcessConfig i o e -> Process m ExitCode-    -- | The absolute path of the currently running executable, for re-invoking-    -- this program as a subprocess.-    GetExecutablePath :: Process m FilePath-    -- | Spawn a process and return its handle. Internal; callers use 'withProcessGroup'.-    StartProcess :: ProcessConfig i o e -> Process m (RunningProcess i o e)-    -- | Terminate the leader and close its streams. Internal; does not reach the-    -- rest of the group.-    StopProcess :: RunningProcess i o e -> Process m ()-    -- | Block until the process exits and return its exit code.-    WaitExitCode :: RunningProcess i o e -> Process m ExitCode-    -- | Send SIGINT to the process's group. Requires @'setCreateGroup' True@.-    InterruptProcessGroup :: RunningProcess i o e -> Process m ()-    -- | Look up the OS process id, or 'Nothing' if it has already exited. Internal.-    GetProcessId :: RunningProcess i o e -> Process m (Maybe Pid)-    -- | Send SIGTERM to the given process group. Internal.-    SignalProcessGroupTerm :: Pid -> Process m ()+data Process :: Effect  -makeEffect ''Process+type instance DispatchOf Process = Static 'WithSideEffects  +data instance StaticRep Process = Process++ -- | Run a process in its own process group, terminating the /whole group/ — the -- process and every descendant it spawned — when the body returns or throws. --@@ -159,16 +139,58 @@         _ -> Nothing  -runProcessIO :: (IOE :> es) => Eff (Process : es) a -> Eff es a-runProcessIO = interpret_ \case-    ReadProcessStdout cfg -> liftIO $ TP.readProcessStdout cfg-    ReadProcess cfg -> liftIO $ TP.readProcess cfg-    RunProcess cfg -> liftIO $ TP.runProcess cfg-    GetExecutablePath -> liftIO Env.getExecutablePath-    StartProcess cfg -> liftIO $ RunningProcess <$> TP.startProcess cfg-    StopProcess (RunningProcess p) -> liftIO $ TP.stopProcess p-    WaitExitCode (RunningProcess p) -> liftIO $ TP.waitExitCode p-    InterruptProcessGroup (RunningProcess p) -> liftIO $ interruptProcessGroupOf (TP.unsafeProcessHandle p)-    GetProcessId (RunningProcess p) -> liftIO $ getPid (TP.unsafeProcessHandle p)-    SignalProcessGroupTerm pid ->-        liftIO $ signalProcessGroup sigTERM pid `catch` \(_ :: IOException) -> pure ()+-- | Run a process to completion, returning its exit code and captured stdout.+readProcessStdout :: (Process :> es) => ProcessConfig i o e -> Eff es (ExitCode, LByteString)+readProcessStdout = unsafeEff_ . TP.readProcessStdout+++-- | Run a process to completion, capturing both stdout and stderr along with+-- its exit code.+readProcess :: (Process :> es) => ProcessConfig i o e -> Eff es (ExitCode, LByteString, LByteString)+readProcess = unsafeEff_ . TP.readProcess+++runProcess :: (Process :> es) => ProcessConfig i o e -> Eff es ExitCode+runProcess = unsafeEff_ . TP.runProcess+++-- | The absolute path of the currently running executable, for re-invoking+-- this program as a subprocess.+getExecutablePath :: (Process :> es) => Eff es FilePath+getExecutablePath = unsafeEff_ Env.getExecutablePath+++-- | Spawn a process and return its handle. Internal; callers use 'withProcessGroup'.+startProcess :: (Process :> es) => ProcessConfig i o e -> Eff es (RunningProcess i o e)+startProcess = unsafeEff_ . fmap RunningProcess . TP.startProcess+++-- | Terminate the leader and close its streams. Internal; does not reach the+-- rest of the group.+stopProcess :: (Process :> es) => RunningProcess i o e -> Eff es ()+stopProcess = unsafeEff_ . TP.stopProcess . getRunningProcess+++-- | Block until the process exits and return its exit code.+waitExitCode :: (Process :> es) => RunningProcess i o e -> Eff es ExitCode+waitExitCode = unsafeEff_ . TP.waitExitCode . getRunningProcess+++-- | Send SIGINT to the process's group. Requires @'setCreateGroup' True@.+interruptProcessGroup :: (Process :> es) => RunningProcess i o e -> Eff es ()+interruptProcessGroup = unsafeEff_ . interruptProcessGroupOf . TP.unsafeProcessHandle . getRunningProcess+++-- | Look up the OS process id, or 'Nothing' if it has already exited. Internal.+getProcessId :: (Process :> es) => RunningProcess i o e -> Eff es (Maybe Pid)+getProcessId = unsafeEff_ . getPid . TP.unsafeProcessHandle . getRunningProcess+++-- | Send SIGTERM to the given process group. Internal.+signalProcessGroupTerm :: (Process :> es) => Pid -> Eff es ()+signalProcessGroupTerm pid =+    unsafeEff_ $ signalProcessGroup sigTERM pid `catch` \(_ :: IOException) -> pure ()+++run :: (IOE :> es) => Eff (Process : es) a -> Eff es a+run = evalStaticRep Process
src/Atelier/Effects/Process/Internal.hs view
@@ -13,4 +13,4 @@  -- | A process run in its own group by 'Atelier.Effects.Process.withProcessGroup'. -- Parameterised by its stdin, stdout and stderr stream types.-newtype RunningProcess i o e = RunningProcess (TP.Process i o e)+newtype RunningProcess i o e = RunningProcess {getRunningProcess :: TP.Process i o e}
src/Atelier/Effects/PubSub.hs view
@@ -19,18 +19,18 @@      . (Chan :> es, Clock :> es)     => Eff (Pub event : Sub event : es) a -> Eff es a run action = do-    (inChan, _) <- Chan.newChan @(UTCTime, event)+    (inChan, _) <- Chan.new @(UTCTime, event)     let handlePub = interpret_ \case             Publish event -> do                 timestamp <- Clock.currentTime-                Chan.writeChan inChan (timestamp, event)+                Chan.write inChan (timestamp, event)          handleSub = interpret \env -> \case             ListenWith onSubscribed listener -> localSeqUnlift env \unlift -> do-                chan <- Chan.dupChan inChan+                chan <- Chan.dup inChan                 unlift onSubscribed                 forever do-                    (timestamp, event) <- Chan.readChan chan+                    (timestamp, event) <- Chan.read chan                     unlift $ listener timestamp event      handleSub . handlePub $ action
src/Atelier/Effects/PubSub/Pub.hs view
@@ -4,7 +4,7 @@     , publish        -- * Interpeters-    , runNoOp+    , noOp     , toWriter     , map     , mapM@@ -28,8 +28,8 @@ makeEffect ''Pub  -runNoOp :: forall event es a. Eff (Pub event : es) a -> Eff es a-runNoOp = interpret_ \(Publish _) -> pure ()+noOp :: forall event es a. Eff (Pub event : es) a -> Eff es a+noOp = interpret_ \(Publish _) -> pure ()   -- | Handler that uses a provided Writer effect instead of actually publishing.
src/Atelier/Effects/PubSub/Sub.hs view
@@ -152,4 +152,6 @@ data OnceEx ev = OnceEx ev  -instance Show (OnceEx ev) where show _ = "OnceEx"+instance Show (OnceEx ev) where+    show :: OnceEx ev -> String+    show _ = "OnceEx"
− src/Atelier/Effects/Publishing.hs
@@ -1,18 +0,0 @@-module Atelier.Effects.Publishing (runPubSub) where--import Atelier.Effects.Chan (Chan)-import Atelier.Effects.Clock (Clock)-import Atelier.Effects.PubSub.Pub (Pub)-import Atelier.Effects.PubSub.Sub (Sub)--import Atelier.Effects.PubSub qualified as PubSub----- | Runs 'Pub' and 'Sub' effects with an internal channel for a specific event--- type.-runPubSub-    :: forall event es a-     . (Chan :> es, Clock :> es)-    => Eff (Pub event : Sub event : es) a -> Eff es a-runPubSub = PubSub.run-{-# DEPRECATED runPubSub "Use 'Atelier.Effects.PubSub.run' instead." #-}
− src/Atelier/Effects/Publishing/Pub.hs
@@ -1,8 +0,0 @@-module Atelier.Effects.Publishing.Pub-    {-# DEPRECATED "Use 'Atelier.Effects.PubSub.Pub' instead." #-}-    ( module Pub-    )-where--import Atelier.Effects.PubSub.Pub as Pub-
− src/Atelier/Effects/Publishing/Sub.hs
@@ -1,8 +0,0 @@-module Atelier.Effects.Publishing.Sub-    {-# DEPRECATED "Use 'Atelier.Effects.PubSub.Sub' instead." #-}-    ( module Sub-    )-where--import Atelier.Effects.PubSub.Sub as Sub-
src/Atelier/Effects/Tally.hs view
@@ -1,38 +1,12 @@ -- | Tally tracking effect for counting occurrences per key------ Tracks the total number of occurrences per key, delegating storage and--- eviction to a 'Cache' interpreter.------ The effect is polymorphic over both the key type and the status type returned--- by a caller-supplied classifier function. This allows callers to define their--- own thresholds and result types rather than relying on a single hard limit.------ == Basic Usage------ @--- checkDuplicate :: (Tally DuplicateKey :> es) => DuplicateKey -> Config -> Eff es ()--- checkDuplicate key cfg =---     withTallyCheck key classify $ \case---         Nothing      -> pure ()---         Just Minor   -> warnAboutPeer---         Just Critical -> banPeer---   where---     classify c---         | c > cfg.criticalThreshold = Just Critical---         | c > cfg.warningThreshold  = Just Minor---         | otherwise                 = Nothing--- @ module Atelier.Effects.Tally     ( -- * Effect       Tally-    , withTallyCheck--      -- * Re-exports-    , module Atelier.Effects.Cache.Config+    , count        -- * Interpreters-    , runTally-    , runTallyConst+    , run+    , runConst     ) where @@ -42,30 +16,43 @@ import Effectful.Reader.Static (Reader) import Effectful.TH (makeEffect) -import Atelier.Effects.Cache (cacheModify, runCacheTtl) import Atelier.Effects.Cache.Config import Atelier.Effects.Clock (Clock) import Atelier.Effects.Conc (Conc) import Atelier.Effects.Delay (Delay) import Atelier.Effects.Log (Log) +import Atelier.Effects.Cache qualified as Cache + -- | Tally tracking effect parameterized by key type data Tally key :: Effect where-    -- | Increment the hit count for a key and run an action based on a classifier.+    -- | Increment the hit count for a key and run an action with said count.     ---    -- The classifier receives the new count and produces a status value of any-    -- type the caller chooses. The continuation then receives that status.-    WithTallyCheck+    -- @+    -- checkDuplicate :: (Tally DuplicateKey :> es) => DuplicateKey -> Config -> Eff es ()+    -- checkDuplicate key cfg =+    --     withTallyCheck key $ action . classify+    --   where+    --     action = \case+    --         Nothing      -> pure ()+    --         Just Minor   -> warnAboutPeer+    --         Just Critical -> banPeer+    --     classify c+    --         | c > cfg.criticalThreshold = Just Critical+    --         | c > cfg.warningThreshold  = Just Minor+    --         | otherwise                 = Nothing+    -- @+    Count         :: (Hashable key, Ord key)-        => key -> (Int -> status) -> (status -> m a) -> Tally key m a+        => key -> (Int -> m a) -> Tally key m a   makeEffect ''Tally   -- | Run the Tally effect, using a TTL-evicting cache as the backing store-runTally+run     :: forall key es a      . ( Clock :> es        , Conc :> es@@ -77,15 +64,15 @@        )     => Eff (Tally key : es) a     -> Eff es a-runTally = reinterpret (runCacheTtl @key @Int) $ \env -> \case-    WithTallyCheck key classify continuation -> localSeqUnlift env $ \unlift -> do-        count <- cacheModify key (maybe 1 (+ 1))-        unlift $ continuation (classify count)+run = reinterpret (Cache.runTTL @key @Int) $ \env -> \case+    Count key continuation -> localSeqUnlift env $ \unlift -> do+        c <- Cache.modify key (maybe 1 (+ 1))+        unlift $ continuation c   -- | Interpret 'Tally' with a fixed count for every key, ignoring real -- tallying. The classifier is always handed @c@, making the outcome -- deterministic for tests.-runTallyConst :: Int -> Eff (Tally key : es) a -> Eff es a-runTallyConst c = interpret \env -> \case-    WithTallyCheck _ classify f -> localSeqUnlift env \unlift -> unlift $ f (classify c)+runConst :: Int -> Eff (Tally key : es) a -> Eff es a+runConst c = interpret \env -> \case+    Count _ f -> localSeqUnlift env \unlift -> unlift $ f c
src/Atelier/Effects/Timeout.hs view
@@ -1,27 +1,28 @@ -- | An effect for bounding a computation by a wall-clock deadline.------ 'timeout' runs an action, returning 'Nothing' if it does not finish within--- the given 'TimeUnit' duration; 'timeout_' is the variant that discards the--- result. When the deadline passes the action is cancelled with an asynchronous--- exception.------ @--- result <- timeout (5 :: Second) fetchRemote--- case result of---     Just value -> use value---     Nothing    -> logWarn \"fetch timed out\"--- @ module Atelier.Effects.Timeout     ( Timeout-    , timeout-    , timeout_-    , runTimeout+    , with+    , with_+    , run     ) where -import Effectful (Effect, IOE, Limit (..), Persistence (..), UnliftStrategy (..))-import Effectful.Dispatch.Dynamic (interpret, localUnliftIO)-import Effectful.TH (makeEffect)+import Effectful+    ( Dispatch (..)+    , DispatchOf+    , Effect+    , IOE+    , Limit (..)+    , Persistence (..)+    )+import Effectful.Dispatch.Static+    ( SideEffects (..)+    , StaticRep+    , concUnliftIO+    , evalStaticRep+    , getStaticRep+    , unsafeEff+    )  import System.Timeout qualified as IO @@ -29,25 +30,44 @@   -- | Effect for bounding a computation by a wall-clock deadline.-data Timeout :: Effect where-    -- | Run an action, returning 'Just' its result if it completes within the-    -- duration, or 'Nothing' if the deadline passes first.-    Timeout :: (TimeUnit t) => t -> m a -> Timeout m (Maybe a)+data Timeout :: Effect  -makeEffect ''Timeout+type instance DispatchOf Timeout = Static 'WithSideEffects  +data instance StaticRep Timeout = Timeout+++-- | Run an action, returning 'Just' its result if it completes within the+-- duration, or 'Nothing' if the deadline passes first.+--+-- 'timeout' runs an action, returning 'Nothing' if it does not finish within+-- the given 'TimeUnit' duration; 'timeout_' is the variant that discards the+-- result. When the deadline passes the action is cancelled with an asynchronous+-- exception.+--+-- @+-- result <- Timeout.with (5 :: Second) fetchRemote+-- case result of+--     Just value -> use value+--     Nothing    -> logWarn \"fetch timed out\"+-- @+with :: (TimeUnit t, Timeout :> es) => t -> Eff es a -> Eff es (Maybe a)+with delay action = do+    Timeout <- getStaticRep+    unsafeEff \env -> do+        concUnliftIO env Persistent (Limited 1) \unlift ->+            IO.timeout (fromIntegral $ toMicroseconds delay) $ unlift action++ -- | Like 'timeout', but for times where you do not need the result. If you -- need to distinguish whether the action ran to completion, or whether it -- timed out, you should use 'timeout'.-timeout_ :: (TimeUnit t, Timeout :> es) => t -> Eff es a -> Eff es ()-timeout_ t m = const () <$> timeout t m+with_ :: (TimeUnit t, Timeout :> es) => t -> Eff es a -> Eff es ()+with_ t m = const () <$> with t m   -- | Interpret 'Timeout' using 'System.Timeout.timeout'.-runTimeout :: (HasCallStack, IOE :> es) => Eff (Timeout : es) a -> Eff es a-runTimeout = interpret \env -> \case-    Timeout delay action ->-        localUnliftIO env (ConcUnlift Persistent (Limited 1)) \unlift ->-            IO.timeout (fromIntegral $ toMicroseconds delay) $ unlift action+run :: (HasCallStack, IOE :> es) => Eff (Timeout : es) a -> Eff es a+run = evalStaticRep Timeout
− src/Atelier/Effects/UUID.hs
@@ -1,38 +0,0 @@--- | An effect for generating UUIDs.------ 'runGenUUID' produces fresh random (version 4) UUIDs; 'runGenUUIDConst'--- returns a fixed UUID every time, for deterministic tests. The 'UUID' type is--- re-exported from @uuid@.-module Atelier.Effects.UUID-    ( GenUUID-    , UUID-    , gen-    , runGenUUID-    , runGenUUIDConst-    )-where--import Data.UUID (UUID)-import Data.UUID.V4 (nextRandom)-import Effectful (Effect, IOE)-import Effectful.Dispatch.Dynamic (interpret_)-import Effectful.TH (makeEffect)----- | Effect for generating UUIDs.-data GenUUID :: Effect where-    -- | Generate a UUID.-    Gen :: GenUUID m UUID---makeEffect ''GenUUID----- | Interpret 'GenUUID' by generating fresh random (version 4) UUIDs.-runGenUUID :: (IOE :> es) => Eff (GenUUID : es) a -> Eff es a-runGenUUID = interpret_ \Gen -> liftIO nextRandom----- | Interpret 'GenUUID' so 'gen' always returns the given fixed UUID, for tests.-runGenUUIDConst :: UUID -> Eff (GenUUID : es) a -> Eff es a-runGenUUIDConst uuid = interpret_ \Gen -> pure uuid
− src/Atelier/Types/Base64.hs
@@ -1,33 +0,0 @@--- | A 'ByteString' that serialises to and from JSON as a Base64-encoded string.------ Use it directly as a field type, or derive the JSON instances for a--- @ByteString@ newtype via it, to carry binary data through JSON without--- tripping over non-UTF-8 bytes:------ @--- newtype Token = Token ByteString---     deriving (ToJSON, FromJSON) via Base64--- @-module Atelier.Types.Base64-    ( Base64 (..)-    )-where--import Data.Aeson (FromJSON (..), ToJSON (..), withText)--import Data.ByteString.Base64 qualified as Base64----- | A 'ByteString' whose JSON representation is Base64-encoded text.-newtype Base64 = Base64 {getBase64 :: ByteString}---instance ToJSON Base64 where-    toJSON = toJSON . decodeUtf8 @Text . Base64.encode . getBase64---instance FromJSON Base64 where-    parseJSON = withText "Base64" $ \t ->-        case Base64.decode (encodeUtf8 t) of-            Left err -> fail err-            Right b -> pure (Base64 b)
+ src/Atelier/Types/JSON/Base64.hs view
@@ -0,0 +1,33 @@+-- | A 'ByteString' that serialises to and from JSON as a Base64-encoded string.+--+-- Use it directly as a field type, or derive the JSON instances for a+-- @ByteString@ newtype via it, to carry binary data through JSON without+-- tripping over non-UTF-8 bytes:+--+-- @+-- newtype Token = Token ByteString+--     deriving (ToJSON, FromJSON) via Base64+-- @+module Atelier.Types.JSON.Base64+    ( Base64 (..)+    )+where++import Data.Aeson (FromJSON (..), ToJSON (..), withText)++import Data.ByteString.Base64 qualified as Base64+++-- | A 'ByteString' whose JSON representation is Base64-encoded text.+newtype Base64 = Base64 {getBase64 :: ByteString}+++instance ToJSON Base64 where+    toJSON = toJSON . decodeUtf8 @Text . Base64.encode . getBase64+++instance FromJSON Base64 where+    parseJSON = withText "Base64" $ \t ->+        case Base64.decode (encodeUtf8 t) of+            Left err -> fail err+            Right b -> pure (Base64 b)
+ src/Atelier/Types/JSON/Renamed.hs view
@@ -0,0 +1,23 @@+module Atelier.Types.JSON.Renamed (Renamed) where++import Data.Aeson (FromJSON (..), ToJSON (..))+import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)+++data Renamed (from :: Symbol) (to :: Symbol)+    deriving stock (Eq, Generic, Show)+++instance ToJSON (Renamed from to) where+    toJSON = \case {}+    toEncoding = \case {}+++instance forall from to. (KnownSymbol from, KnownSymbol to) => FromJSON (Renamed from to) where+    parseJSON _ =+        fail+            $ "The property '"+                <> symbolVal (Proxy @from)+                <> "' has been renamed or moved to '"+                <> symbolVal (Proxy @to)+                <> "'"
+ src/Atelier/Types/JSON/WithDefaults.hs view
@@ -0,0 +1,40 @@+-- | Merge a type's 'Default' value into incoming JSON before parsing, so fields+-- absent from the input fall back to their defaults instead of failing.+--+-- Derive 'FromJSON' via this wrapper when partial JSON should be completed from+-- a 'Default' instance. Missing non-'Maybe' fields then take their default+-- value rather than causing a parse error:+--+-- @+-- data Config = Config { retries :: Int, verbose :: Bool }+--     deriving stock (Generic)+--     deriving (FromJSON) via (WithDefaults Config)+--+-- instance Default Config where+--     def = Config { retries = 3, verbose = False }+-- @+--+-- Parsing @{\"verbose\": true}@ then yields @Config { retries = 3, verbose = True }@.+module Atelier.Types.JSON.WithDefaults+    ( WithDefaults (..)+    )+where++import Data.Aeson (FromJSON (..), ToJSON (..), Value (..))+import Data.Default (Default (..))++import Data.Aeson.KeyMap qualified as KM+++-- | Carrier that fills missing JSON fields from a type's 'Default' instance+-- before parsing.+newtype WithDefaults a = WithDefaults {getWithDefaults :: a}+++instance (Default a, FromJSON a, ToJSON a) => FromJSON (WithDefaults a) where+    parseJSON v = WithDefaults <$> parseJSON merged+      where+        merged = mergeLeft (toJSON (def :: a)) v++        mergeLeft (Object l) (Object r) = Object $ KM.unionWith mergeLeft l r+        mergeLeft _ r = r
− src/Atelier/Types/JsonReadShow.hs
@@ -1,12 +0,0 @@-module Atelier.Types.JsonReadShow-    {-# DEPRECATED "Use Atelier.Types.JSON.ReadShow instead" #-}-    (JsonReadShow (..))-where--import Data.Aeson (FromJSON, ToJSON)--import Atelier.Types.JSON.ReadShow (ReadShow (..))---newtype JsonReadShow a = JsonReadShow {getJsonReadShow :: a}-    deriving (FromJSON, ToJSON) via ReadShow a
− src/Atelier/Types/QuietSnake.hs
@@ -1,8 +0,0 @@-module Atelier.Types.QuietSnake-    {-# DEPRECATED "Renamed to Atelier.Types.JSON.QuietSnake" #-}-    ( QuietSnake (..)-    )-where--import Atelier.Types.JSON.QuietSnake (QuietSnake (..))-
− src/Atelier/Types/WithDefaults.hs
@@ -1,40 +0,0 @@--- | Merge a type's 'Default' value into incoming JSON before parsing, so fields--- absent from the input fall back to their defaults instead of failing.------ Derive 'FromJSON' via this wrapper when partial JSON should be completed from--- a 'Default' instance. Missing non-'Maybe' fields then take their default--- value rather than causing a parse error:------ @--- data Config = Config { retries :: Int, verbose :: Bool }---     deriving stock (Generic)---     deriving (FromJSON) via (WithDefaults Config)------ instance Default Config where---     def = Config { retries = 3, verbose = False }--- @------ Parsing @{\"verbose\": true}@ then yields @Config { retries = 3, verbose = True }@.-module Atelier.Types.WithDefaults-    ( WithDefaults (..)-    )-where--import Data.Aeson (FromJSON (..), ToJSON (..), Value (..))-import Data.Default (Default (..))--import Data.Aeson.KeyMap qualified as KM----- | Carrier that fills missing JSON fields from a type's 'Default' instance--- before parsing.-newtype WithDefaults a = WithDefaults {getWithDefaults :: a}---instance (Default a, FromJSON a, ToJSON a) => FromJSON (WithDefaults a) where-    parseJSON v = WithDefaults <$> parseJSON merged-      where-        merged = mergeLeft (toJSON (def :: a)) v--        mergeLeft (Object l) (Object r) = Object $ KM.unionWith mergeLeft l r-        mergeLeft _ r = r
test/Unit/Atelier/ConfigSpec.hs view
@@ -11,7 +11,7 @@  import Atelier.Config (LoadedConfig (..), extractNestedConfig) import Atelier.Types.JSON.QuietSnake (QuietSnake (..))-import Atelier.Types.WithDefaults (WithDefaults (..))+import Atelier.Types.JSON.WithDefaults (WithDefaults (..))   test_Config :: TestTree
+ test/Unit/Atelier/Effects/Arguments/SwitchSpec.hs view
@@ -0,0 +1,66 @@+module Unit.Atelier.Effects.Arguments.SwitchSpec (test_Switch) where++import Options.Applicative+    ( Parser+    , ParserResult (..)+    , defaultPrefs+    , execParserPure+    , help+    , helper+    , info+    , long+    , renderFailure+    , short+    )+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit (assertFailure, testCase, (@?=))++import Data.List qualified as List++import Atelier.Effects.Arguments.Switch qualified as Switch+++useCurrent :: Parser Bool+useCurrent = Switch.parser (long "use-current" <> short 'c' <> help "Use the current status.")+++parse :: [String] -> Maybe Bool+parse args = case execParserPure defaultPrefs (info useCurrent mempty) args of+    Success b -> Just b+    _ -> Nothing+++helpText :: IO String+helpText = case execParserPure defaultPrefs (info (useCurrent <**> helper) mempty) ["--help"] of+    Failure failure -> pure $ fst $ renderFailure failure "prog"+    _ -> assertFailure "expected --help to produce help text"+++test_Switch :: TestTree+test_Switch =+    testGroup+        "Switch"+        [ testCase "defaults to False" do+            parse [] @?= Just False+        , testCase "--<name> enables" do+            parse ["--use-current"] @?= Just True+        , testCase "short name enables" do+            parse ["-c"] @?= Just True+        , testCase "--no-<name> disables" do+            parse ["--no-use-current"] @?= Just False+        , testCase "last flag wins when enabling" do+            parse ["--no-use-current", "--use-current"] @?= Just True+        , testCase "last flag wins when disabling" do+            parse ["-c", "--no-use-current"] @?= Just False+        , testCase "last flag wins across many flags" do+            parse ["--use-current", "--no-use-current", "-c", "--no-use-current"] @?= Just False+        , testCase "help shows the combined flag" do+            text <- helpText+            List.lines text+                @?= [ "Usage: prog [-c,--[no-]use-current]"+                    , ""+                    , "Available options:"+                    , "  -c,--[no-]use-current    Use the current status."+                    , "  -h,--help                Show this help text"+                    ]+        ]
test/Unit/Atelier/Effects/AwaitSpec.hs view
@@ -10,10 +10,9 @@ import Data.List qualified as List import Effectful.Concurrent.STM qualified as STM -import Atelier.Effects.Chan (runChan)-import Atelier.Effects.Conc (runConc)- import Atelier.Effects.Await qualified as Await+import Atelier.Effects.Chan qualified as Chan+import Atelier.Effects.Conc qualified as Conc import Atelier.Effects.Yield qualified as Yield  @@ -101,7 +100,7 @@         ]     ]   where-    runTest = runEff . runConcurrent . runConc . runChan . runWriter+    runTest = runEff . runConcurrent . Conc.runIO . Chan.run . runWriter     -- Block the current (forked) computation indefinitely; used to keep a yielder     -- alive so the awaiter wins the awaitYield termination race. The explicit     -- signature keeps it polymorphic across the differing effect stacks below.
test/Unit/Atelier/Effects/Cache/SingleflightSpec.hs view
@@ -8,9 +8,14 @@ import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (testCase, (@?=)) -import Atelier.Effects.Cache.Singleflight (Singleflight, runSingleflight, updateCache, withCache)-import Atelier.Effects.Conc (Conc, runConc)-import Atelier.Effects.Delay (Delay, runDelay)+import Atelier.Effects.Cache.Singleflight+    ( Singleflight+    , runIO+    , update+    , with+    )+import Atelier.Effects.Conc (Conc)+import Atelier.Effects.Delay (Delay) import Atelier.Time (Millisecond) import Atelier.Types.Semaphore (Semaphore) @@ -32,10 +37,10 @@ runSingleflightTest action =     runEff         . runConcurrent-        . runConc-        . runDelay+        . Conc.runIO+        . Delay.runIO         . runState @Int 0-        . runSingleflight @Int @Int+        . runIO @Int @Int         $ action  @@ -64,7 +69,7 @@                 "First request executes computation"                 [ testCase "executes the computation on first request" $ do                     (result, execCount) <- runSingleflightTest $ do-                        withCache 1 (compute 42)+                        with 1 (compute 42)                     result @?= 42                     execCount @?= 1                 ]@@ -72,16 +77,16 @@                 "Cached value returned on second request"                 [ testCase "returns cached value without re-executing" $ do                     (result, execCount) <- runSingleflightTest $ do-                        r1 <- withCache 1 (compute 42)-                        r2 <- withCache 1 (compute 42)+                        r1 <- with 1 (compute 42)+                        r2 <- with 1 (compute 42)                         pure (r1, r2)                     result @?= (42, 42)                     execCount @?= 1                 , testCase "returns cached value across multiple sequential requests" $ do                     (results, execCount) <- runSingleflightTest $ do-                        r1 <- withCache 1 (compute 42)-                        r2 <- withCache 1 (compute 42)-                        r3 <- withCache 1 (compute 42)+                        r1 <- with 1 (compute 42)+                        r2 <- with 1 (compute 42)+                        r3 <- with 1 (compute 42)                         pure [r1, r2, r3]                     results @?= [42, 42, 42]                     execCount @?= 1@@ -93,7 +98,7 @@                         -- Launch 10 concurrent requests for the same key                         asyncs <- replicateM 10 do                             sem <- Sem.new-                            async <- Conc.fork $ withCache 1 (slowCompute sem 42)+                            async <- Conc.fork $ with 1 (slowCompute sem 42)                             pure (sem, async)                         traverse_ Sem.set $ fst <$> asyncs                         traverse Conc.await $ snd <$> asyncs@@ -104,10 +109,10 @@                     (results, execCount) <- runSingleflightTest $ do                         sem <- Sem.new                         -- Launch concurrent requests with different delays-                        a1 <- Conc.fork $ withCache 1 (slowCompute sem 99)+                        a1 <- Conc.fork $ with 1 (slowCompute sem 99)                         Delay.wait (1 :: Millisecond) -- Ensure first request starts-                        a2 <- Conc.fork $ withCache 1 (compute 99)-                        a3 <- Conc.fork $ withCache 1 (compute 99)+                        a2 <- Conc.fork $ with 1 (compute 99)+                        a3 <- Conc.fork $ with 1 (compute 99)                         Sem.signal sem -- Let first request continue                         r1 <- Conc.await a1                         r2 <- Conc.await a2@@ -120,18 +125,18 @@                 "Different keys execute independently"                 [ testCase "executes separate computations for different keys" $ do                     (results, execCount) <- runSingleflightTest $ do-                        r1 <- withCache 1 (compute 10)-                        r2 <- withCache 2 (compute 20)-                        r3 <- withCache 3 (compute 30)+                        r1 <- with 1 (compute 10)+                        r2 <- with 2 (compute 20)+                        r3 <- with 3 (compute 30)                         pure [r1, r2, r3]                     results @?= [10, 20, 30]                     execCount @?= 3                 , testCase "different keys can run concurrently" $ do                     (results, execCount) <- runSingleflightTest $ do                         sem <- Sem.newSet-                        a1 <- Conc.fork $ withCache 1 (slowCompute sem 10)-                        a2 <- Conc.fork $ withCache 2 (slowCompute sem 20)-                        a3 <- Conc.fork $ withCache 3 (slowCompute sem 30)+                        a1 <- Conc.fork $ with 1 (slowCompute sem 10)+                        a2 <- Conc.fork $ with 2 (slowCompute sem 20)+                        a3 <- Conc.fork $ with 3 (slowCompute sem 30)                         replicateM_ 3 $ Sem.signal sem                         r1 <- Conc.await a1                         r2 <- Conc.await a2@@ -144,15 +149,15 @@                 "UpdateCache pre-populates the cache"                 [ testCase "returns pre-populated value without executing computation" $ do                     (result, execCount) <- runSingleflightTest $ do-                        updateCache [(1, 99)]-                        withCache 1 (compute 42)+                        update [(1, 99)]+                        with 1 (compute 42)                     result @?= 99                     execCount @?= 0                 , testCase "pre-populated values are returned by subsequent requests" $ do                     (results, execCount) <- runSingleflightTest $ do-                        updateCache [(1, 99)]-                        r1 <- withCache 1 (compute 42)-                        r2 <- withCache 1 (compute 42)+                        update [(1, 99)]+                        r1 <- with 1 (compute 42)+                        r2 <- with 1 (compute 42)                         pure [r1, r2]                     results @?= [99, 99]                     execCount @?= 0@@ -161,10 +166,10 @@                 "UpdateCache handles multiple entries"                 [ testCase "correctly handles multiple key-value pairs" $ do                     (results, execCount) <- runSingleflightTest $ do-                        updateCache [(1, 10), (2, 20), (3, 30)]-                        r1 <- withCache 1 (compute 99)-                        r2 <- withCache 2 (compute 99)-                        r3 <- withCache 3 (compute 99)+                        update [(1, 10), (2, 20), (3, 30)]+                        r1 <- with 1 (compute 99)+                        r2 <- with 2 (compute 99)+                        r3 <- with 3 (compute 99)                         pure [r1, r2, r3]                     results @?= [10, 20, 30]                     execCount @?= 0@@ -176,7 +181,7 @@                         -- Start one slow computation and many fast waiters                         asyncs <- replicateM 20 do                             sem <- Sem.new-                            async <- Conc.fork $ withCache 1 (slowCompute sem 777)+                            async <- Conc.fork $ with 1 (slowCompute sem 777)                             pure (sem, async)                         traverse_ Sem.signal $ fst <$> asyncs                         traverse Conc.await $ snd <$> asyncs@@ -191,26 +196,26 @@                 "Computation throws exception"                 [ testCase "propagates exception to the first caller" $ do                     let action = runSingleflightTest $ do-                            withCache @Int @Int 1 (throwIO $ TestException "boom")+                            with @Int @Int 1 (throwIO $ TestException "boom")                     result <- try action                     result @?= Left (TestException "boom")                 , testCase "exception does not get cached" $ do                     (result, execCount) <- runSingleflightTest $ do                         -- First request throws                         _ <--                            (withCache @Int @Int 1 (throwIO $ TestException "boom"))+                            (with @Int @Int 1 (throwIO $ TestException "boom"))                                 `catch` \(_ :: TestException) -> pure 0                         -- Second request should re-execute-                        withCache 1 (compute 42)+                        with 1 (compute 42)                     result @?= 42                     execCount @?= 1                 , testCase "propagates exception to all concurrent waiters" $ do                     let action = runSingleflightTest $ do                             sem <- Sem.new                             a1 <--                                Conc.fork $ withCache @Int @Int 1 (slowCompute sem 42 >> throwIO (TestException "concurrent-boom"))+                                Conc.fork $ with @Int @Int 1 (slowCompute sem 42 >> throwIO (TestException "concurrent-boom"))                             Delay.wait (1 :: Millisecond)-                            a2 <- Conc.fork $ withCache @Int @Int 1 (compute 99)+                            a2 <- Conc.fork $ with @Int @Int 1 (compute 99)                             Sem.signal sem                             _ <- Conc.await a1                             Conc.await a2@@ -223,10 +228,10 @@                     (result, execCount) <- runSingleflightTest $ do                         sem <- Sem.new                         -- Start slow computation-                        a1 <- Conc.fork $ withCache 1 (slowCompute sem 42)+                        a1 <- Conc.fork $ with 1 (slowCompute sem 42)                         Delay.wait (1 :: Millisecond) -- Let it start                         -- Update cache while computation is running-                        updateCache [(1, 999)]+                        update [(1, 999)]                         -- Both should get the updated value                         Sem.signal sem                         Conc.await a1@@ -236,12 +241,12 @@                     (results, execCount) <- runSingleflightTest $ do                         sem <- Sem.new                         -- Start slow computation and waiters-                        a1 <- Conc.fork $ withCache 1 (slowCompute sem 42)+                        a1 <- Conc.fork $ with 1 (slowCompute sem 42)                         Delay.wait (1 :: Millisecond)-                        a2 <- Conc.fork $ withCache 1 (compute 42)+                        a2 <- Conc.fork $ with 1 (compute 42)                         Delay.wait (1 :: Millisecond)                         -- Update while they're all waiting/running-                        updateCache [(1, 888)]+                        update [(1, 888)]                         Sem.signal sem                         r1 <- Conc.await a1                         r2 <- Conc.await a2
test/Unit/Atelier/Effects/CacheSpec.hs view
@@ -3,32 +3,28 @@ import Data.Time (UTCTime (..), addUTCTime, fromGregorian) import Data.Time.Clock (NominalDiffTime) import Effectful (runEff)-import Effectful.Concurrent (runConcurrent)-import Effectful.Reader.Static (runReader)-import Effectful.State.Static.Shared (evalState, modify)+import Effectful.State.Static.Shared (modify) import Hedgehog (forAll, property, (===)) import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (testCase, (@?=)) import Test.Tasty.Hedgehog (testProperty) +import Effectful.Concurrent qualified as Concurrent import Effectful.Concurrent.STM qualified as STM+import Effectful.Reader.Static qualified as Reader+import Effectful.State.Static.Shared qualified as State import Hedgehog.Gen qualified as Gen import Hedgehog.Range qualified as Range -import Atelier.Effects.Cache-    ( Config (..)-    , cacheDelete-    , cacheInsert-    , cacheLookup-    , cacheModify-    , runCacheTtlWithWait-    )-import Atelier.Effects.Clock (runClockState)-import Atelier.Effects.Conc (runConc)-import Atelier.Effects.Delay (runDelay)-import Atelier.Effects.Log (runLogNoOp)+import Atelier.Effects.Cache (Config (..)) +import Atelier.Effects.Cache qualified as Cache+import Atelier.Effects.Clock qualified as Clock+import Atelier.Effects.Conc qualified as Conc+import Atelier.Effects.Delay qualified as Delay+import Atelier.Effects.Log qualified as Log + test_Cache :: TestTree test_Cache =     testGroup@@ -36,53 +32,53 @@         [ testGroup             "Basic Operations"             [ testCase "lookup on absent key returns Nothing" do-                result <- runCacheTest $ cacheLookup 1+                result <- runCacheTest $ Cache.lookup 1                 result @?= Nothing             , testCase "lookup after insert returns Just value" do                 result <- runCacheTest do-                    cacheInsert 1 42-                    cacheLookup 1+                    Cache.insert 1 42+                    Cache.lookup 1                 result @?= Just 42             , testCase "lookup after delete returns Nothing" do                 result <- runCacheTest $ do-                    cacheInsert 1 42-                    cacheDelete 1-                    cacheLookup 1+                    Cache.insert 1 42+                    Cache.delete 1+                    Cache.lookup 1                 result @?= Nothing             , testCase "insert twice updates value" do                 result <- runCacheTest $ do-                    cacheInsert 1 42-                    cacheInsert 1 99-                    cacheLookup 1+                    Cache.insert 1 42+                    Cache.insert 1 99+                    Cache.lookup 1                 result @?= Just 99             , testCase "delete on absent key is a no-op" do                 result <- runCacheTest $ do-                    cacheDelete 1-                    cacheLookup 1+                    Cache.delete 1+                    Cache.lookup 1                 result @?= Nothing             ]         , testGroup             "Modify"             [ testCase "modify on absent key uses Nothing branch" do-                result <- runCacheTest $ cacheModify @Int @Int 1 (maybe 0 (+ 1))+                result <- runCacheTest $ Cache.modify @Int @Int 1 (maybe 0 (+ 1))                 result @?= 0             , testCase "modify on present key uses Just branch" do                 result <- runCacheTest $ do-                    cacheInsert 1 10-                    cacheModify 1 (maybe 0 (+ 1))+                    Cache.insert 1 10+                    Cache.modify 1 (maybe 0 (+ 1))                 result @?= 11             , testCase "modify returns the new value" do                 result <- runCacheTest $ do-                    _ <- cacheModify 1 (maybe 5 (+ 5))-                    cacheModify 1 (maybe 5 (+ 5))+                    _ <- Cache.modify 1 (maybe 5 (+ 5))+                    Cache.modify 1 (maybe 5 (+ 5))                 result @?= 10             ]         , testGroup             "TTL Eviction"             [ testCase "entry is present before TTL expires" do                 result <- runBase . runCacheTestWithWait (STM.atomically STM.retry) $ do-                    cacheInsert 1 42-                    cacheLookup 1+                    Cache.insert 1 42+                    Cache.lookup 1                 result @?= Just 42             , testCase "entry is evicted after cleanup thread fires past TTL" do                 result <- runBase do@@ -90,11 +86,11 @@                     done <- STM.atomically STM.newEmptyTMVar                     runCacheTestWithWait (cleanupBarrier done trigger) do                         awaitReady done-                        cacheInsert 1 42-                        v1 <- cacheLookup 1+                        Cache.insert 1 42+                        v1 <- Cache.lookup 1                         modify (addUTCTime (ttl + 1))                         stepCleanup trigger done -- run one cleanup, block until it finishes-                        v2 <- cacheLookup 1+                        v2 <- Cache.lookup 1                         pure (v1, v2)                 result @?= (Just 42, Nothing)             , testCase "entry within TTL survives cleanup" do@@ -103,10 +99,10 @@                     done <- STM.atomically STM.newEmptyTMVar                     runCacheTestWithWait (cleanupBarrier done trigger) do                         awaitReady done-                        cacheInsert 1 42+                        Cache.insert 1 42                         modify (addUTCTime (ttl - 1))                         stepCleanup trigger done-                        cacheLookup 1+                        Cache.lookup 1                 result @?= Just 42             , testCase "re-inserted entry retains original TTL window" do                 result <- runBase do@@ -114,19 +110,19 @@                     done <- STM.atomically STM.newEmptyTMVar                     runCacheTestWithWait (cleanupBarrier done trigger) do                         awaitReady done-                        cacheInsert 1 42-                        v1 <- cacheLookup 1+                        Cache.insert 1 42+                        v1 <- Cache.lookup 1                          -- re-insert before TTL expires: updates value but preserves createdAt = t0                         modify (addUTCTime (ttl - 1))                         stepCleanup trigger done-                        cacheInsert 1 99-                        v2 <- cacheLookup 1+                        Cache.insert 1 99+                        v2 <- Cache.lookup 1                          -- advance clock past original TTL                         modify (addUTCTime 2)                         stepCleanup trigger done-                        v3 <- cacheLookup 1+                        v3 <- Cache.lookup 1                          pure (v1, v2, v3)                 -- createdAt was preserved from the first insert, so the entry is expired and evicted@@ -137,17 +133,17 @@             [ testProperty "insert then lookup roundtrips value" $ property do                 v <- forAll $ Gen.int (Range.linear 0 1000)                 result <- liftIO $ runCacheTest $ do-                    cacheInsert 1 v-                    cacheLookup 1+                    Cache.insert 1 v+                    Cache.lookup 1                 result === Just v             , testProperty "distinct keys have independent values" $ property do                 m <- forAll $ Gen.int (Range.linear 1 20)                 n <- forAll $ Gen.int (Range.linear 1 20)                 (a, b) <- liftIO $ runCacheTest $ do-                    cacheInsert 1 m-                    cacheInsert 2 n-                    va <- cacheLookup 1-                    vb <- cacheLookup 2+                    Cache.insert 1 m+                    Cache.insert 2 n+                    va <- Cache.lookup 1+                    vb <- Cache.lookup 2                     pure (va, vb)                 a === Just m                 b === Just n@@ -155,8 +151,8 @@                 keys <- forAll $ Gen.list (Range.linear 1 50) Gen.bool                 -- count inserts per key by interleaving                 counts <- liftIO $ runCacheTest $ do-                    let step True = cacheModify @Int 1 (maybe 1 (+ 1))-                        step False = cacheModify @Int 2 (maybe 1 (+ 1))+                    let step True = Cache.modify @Int 1 (maybe 1 (+ 1))+                        step False = Cache.modify @Int 2 (maybe 1 (+ 1))                     traverse step keys                 let key1Counts = [c | (k, c) <- zip keys counts, k]                     key2Counts = [c | (k, c) <- zip keys counts, not k]@@ -165,28 +161,28 @@             , testProperty "concurrent inserts to different keys don't interfere" $ property do                 n <- forAll $ Gen.int (Range.linear 1 20)                 results <- liftIO $ runCacheTest $ do-                    for_ [1 .. n] \i -> cacheInsert i i-                    traverse (\i -> cacheLookup i) [1 .. n]+                    for_ [1 .. n] \i -> Cache.insert i i+                    traverse (\i -> Cache.lookup i) [1 .. n]                 results === map (Just . id) [1 .. n]             , testProperty "modify is atomic under concurrency" $ property do                 n <- forAll $ Gen.int (Range.linear 1 50)                 finalVal <- liftIO $ runCacheTest $ do-                    for_ [1 .. n] \_ -> cacheModify 1 (maybe 1 (+ 1))-                    cacheLookup 1+                    for_ [1 .. n] \_ -> Cache.modify 1 (maybe 1 (+ 1))+                    Cache.lookup 1                 finalVal === Just n             ]         ]   where-    runBase = runEff . runConcurrent+    runBase = runEff . Concurrent.runConcurrent     runCacheTest = runBase . runCacheTestWithWait (STM.atomically STM.retry)     runCacheTestWithWait wait =-        runLogNoOp-            . runConc-            . runDelay-            . evalState epoch-            . runClockState-            . runReader (Config {entryTtl = ttl, cleanupInterval = 3600})-            . runCacheTtlWithWait @Int @Int wait+        Log.runNoOp+            . Conc.runIO+            . Delay.runIO+            . State.evalState epoch+            . Clock.runState+            . Reader.runReader (Config {entryTtl = ttl, cleanupInterval = 3600})+            . Cache.runTTLWithWait @Int @Int wait     epoch = UTCTime (fromGregorian 1970 1 1) 0     ttl = 3600 :: NominalDiffTime 
test/Unit/Atelier/Effects/ChanSpec.hs view
@@ -10,73 +10,75 @@ import Hedgehog.Gen qualified as Gen import Hedgehog.Range qualified as Range -import Atelier.Effects.Chan (Chan, dupChan, newChan, readChan, readChanBatched, runChan, writeChan)+import Atelier.Effects.Chan (Chan) import Atelier.Time (Millisecond, Second) +import Atelier.Effects.Chan qualified as Chan + test_Chan :: TestTree test_Chan =     testGroup         "Chan"         [ testGroup             "Basic Operations"-            [ testCase "writeChan then readChan roundtrips a value" do+            [ testCase "Chan.write then Chan.read roundtrips a value" do                 result <- runChanTest $ do-                    (inChan, outChan) <- newChan-                    writeChan inChan (42 :: Int)-                    readChan outChan+                    (inChan, outChan) <- Chan.new+                    Chan.write inChan (42 :: Int)+                    Chan.read outChan                 result @?= 42             , testProperty "preserves FIFO order" $ property do                 xs <- forAll $ Gen.list (Range.linear 0 50) (Gen.int Range.linearBounded)                 result <- liftIO $ runChanTest $ do-                    (inChan, outChan) <- newChan-                    traverse_ (writeChan inChan) xs-                    replicateM (length xs) (readChan outChan)+                    (inChan, outChan) <- Chan.new+                    traverse_ (Chan.write inChan) xs+                    replicateM (length xs) (Chan.read outChan)                 result === xs-            , testCase "dupChan creates an independent reader that receives the same messages" do+            , testCase "Chan.dup creates an independent reader that receives the same messages" do                 result <- runChanTest $ do-                    (inChan, outChan1) <- newChan-                    outChan2 <- dupChan inChan-                    writeChan inChan (42 :: Int)-                    v1 <- readChan outChan1-                    v2 <- readChan outChan2+                    (inChan, outChan1) <- Chan.new+                    outChan2 <- Chan.dup inChan+                    Chan.write inChan (42 :: Int)+                    v1 <- Chan.read outChan1+                    v2 <- Chan.read outChan2                     pure (v1, v2)                 result @?= (42, 42)             ]         , testGroup-            "readChanBatched"+            "Chan.readBatched"             [ testGroup                 "when items fill the batch before timeout"                 [ testCase "returns a full batch" do                     result <- runChanTest $ do-                        (inChan, outChan) <- newChan-                        traverse_ (writeChan inChan) [1, 2, 3 :: Int]-                        readChanBatched (1 :: Second) 3 outChan+                        (inChan, outChan) <- Chan.new+                        traverse_ (Chan.write inChan) [1, 2, 3 :: Int]+                        Chan.readBatched (1 :: Second) 3 outChan                     result @?= (1 :| [2, 3])                 , testProperty "caps at batchSize even when more items are available" $ property do                     batchSize <- forAll $ Gen.int (Range.linear 1 20)                     extra <- forAll $ Gen.int (Range.linear 1 10)                     let n = batchSize + extra                     result <- liftIO $ runChanTest $ do-                        (inChan, outChan) <- newChan-                        traverse_ (writeChan inChan) [1 .. n]-                        readChanBatched (1 :: Second) batchSize outChan+                        (inChan, outChan) <- Chan.new+                        traverse_ (Chan.write inChan) [1 .. n]+                        Chan.readBatched (1 :: Second) batchSize outChan                     length result === batchSize                 ]             , testGroup                 "when timeout fires before batch is full"                 [ testCase "returns a singleton when only one item is in the channel" do                     result <- runChanTest $ do-                        (inChan, outChan) <- newChan-                        writeChan inChan (1 :: Int)-                        readChanBatched (1 :: Millisecond) 5 outChan+                        (inChan, outChan) <- Chan.new+                        Chan.write inChan (1 :: Int)+                        Chan.readBatched (1 :: Millisecond) 5 outChan                     result @?= (1 :| [])                 , testCase "returns a partial batch" do                     result <- runChanTest $ do-                        (inChan, outChan) <- newChan-                        writeChan inChan (1 :: Int)-                        writeChan inChan 2-                        readChanBatched (1 :: Millisecond) 5 outChan+                        (inChan, outChan) <- Chan.new+                        Chan.write inChan (1 :: Int)+                        Chan.write inChan 2+                        Chan.readBatched (1 :: Millisecond) 5 outChan                     result @?= (1 :| [2])                 ]             ]@@ -88,4 +90,4 @@ --------------------------------------------------------------------------------  runChanTest :: Eff '[Chan, Timeout, IOE] a -> IO a-runChanTest = runEff . runTimeout . runChan+runChanTest = runEff . runTimeout . Chan.run
test/Unit/Atelier/Effects/Conc/TeardownStressSpec.hs view
@@ -25,12 +25,15 @@ import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (testCase, (@?=)) -import Atelier.Effects.Chan (Chan, runChan)-import Atelier.Effects.Clock (Clock, runClock)-import Atelier.Effects.Conc (Conc, fork, fork_, runConc, scoped)+import Atelier.Effects.Chan (Chan)+import Atelier.Effects.Clock (Clock)+import Atelier.Effects.Conc (Conc) import Atelier.Effects.PubSub.Pub (Pub) import Atelier.Effects.PubSub.Sub (Sub) +import Atelier.Effects.Chan qualified as Chan+import Atelier.Effects.Clock qualified as Clock+import Atelier.Effects.Conc qualified as Conc import Atelier.Effects.Iterator qualified as Iter import Atelier.Effects.PubSub qualified as PubSub import Atelier.Effects.PubSub.Pub qualified as Pub@@ -58,16 +61,16 @@                         timeout testTimeoutMicros                             $ runConcTest                             $ replicateM_ iterations-                            $ scoped (void (fork_ blockForever))+                            $ Conc.scoped (void (Conc.fork_ blockForever))                     completed @?= Just ()                 , testCase "reaps a two-child scope (one parked, one transient) across many scopes" do                     completed <-                         timeout testTimeoutMicros                             $ runConcTest                             $ replicateM_ iterations-                            $ scoped do-                                _ <- fork (liftIO (threadDelay 5))-                                void (fork_ blockForever)+                            $ Conc.scoped do+                                _ <- Conc.fork (liftIO (threadDelay 5))+                                void (Conc.fork_ blockForever)                     completed @?= Just ()                 ]           , -- Faithful repro of the actual full-suite victim: the 'fromEvents' Iterator@@ -85,7 +88,7 @@                             $ runIterTest                             $ replicateM_ iterations                             $ Iter.fromEvents @Int \iter -> do-                                _ <- fork do+                                _ <- Conc.fork do                                     liftIO (threadDelay publishDelayUs)                                     traverse_ Pub.publish [1, 2, 3 :: Int]                                 _ <- replicateM 3 (Iter.next iter)@@ -105,7 +108,7 @@                         completed <-                             timeout testTimeoutMicros                                 $ runConcTest-                                $ scoped (void (fork_ nonAllocatingSpin))+                                $ Conc.scoped (void (Conc.fork_ nonAllocatingSpin))                         completed @?= Just ()                     ]                | runSpin@@ -128,9 +131,9 @@   runConcTest :: Eff '[Conc, Concurrent, IOE] a -> IO a-runConcTest = runEff . runConcurrent . runConc+runConcTest = runEff . runConcurrent . Conc.runIO   -- | Mirrors 'IteratorSpec.runTest' — the full effect stack the real victim runs on. runIterTest :: Eff '[Pub Int, Sub Int, Chan, Clock, Conc, Concurrent, IOE] a -> IO a-runIterTest = runEff . runConcurrent . runConc . runClock . runChan . PubSub.run @Int+runIterTest = runEff . runConcurrent . Conc.runIO . Clock.runIO . Chan.run . PubSub.run @Int
test/Unit/Atelier/Effects/ConcSpec.hs view
@@ -13,8 +13,8 @@ import Hedgehog.Gen qualified as Gen import Hedgehog.Range qualified as Range -import Atelier.Effects.Conc (Conc, await, awaitAll, fork, forkTry, fork_, runConc, scoped)-import Atelier.Effects.Delay (Delay, runDelay)+import Atelier.Effects.Conc (Conc, await, awaitAll, fork, forkTry, fork_, runIO, scoped)+import Atelier.Effects.Delay (Delay) import Atelier.Time (Millisecond)  import Atelier.Effects.Delay qualified as Delay@@ -130,8 +130,8 @@ --------------------------------------------------------------------------------  runTest :: Eff '[Delay, Conc, Concurrent, IOE] a -> IO a-runTest = runEff . runConcurrent . runConc . runDelay+runTest = runEff . runConcurrent . runIO . Delay.runIO   runTestSimple :: Eff '[Conc, Concurrent, IOE] a -> IO a-runTestSimple = runEff . runConcurrent . runConc+runTestSimple = runEff . runConcurrent . runIO
test/Unit/Atelier/Effects/ConsoleSpec.hs view
@@ -4,7 +4,7 @@ import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (testCase, (@?=)) -import Atelier.Effects.Console (runConsoleToList)+import Atelier.Effects.Console (runToList)  import Atelier.Effects.Console qualified as Console @@ -14,35 +14,35 @@     testGroup         "Console"         [ testGroup-            "runConsoleToList"+            "runToList"             [ testGroup                 "when nothing is logged"                 [ testCase "returns an empty list" $ do-                    let (_, msgs) = runPureEff $ runConsoleToList $ pure ()+                    let (_, msgs) = runPureEff $ runToList $ pure ()                     msgs @?= []                 ]             , testGroup                 "when logging once"                 [ testCase "collects a single traced message" $ do-                    let (_, msgs) = runPureEff $ runConsoleToList $ Console.putStr "hello"+                    let (_, msgs) = runPureEff $ runToList $ Console.putStr "hello"                     msgs @?= ["hello"]                 ]             , testCase "collects multiple logged messages in order" $ do                 let (_, msgs) =-                        runPureEff . runConsoleToList $ do+                        runPureEff . runToList $ do                             Console.putStr "first"                             Console.putStr "second"                             Console.putStr "third"                 msgs @?= ["first", "second", "third"]             , testCase "returns the result alongside the traced messages" $ do                 let (result, msgs) =-                        runPureEff . runConsoleToList $ do+                        runPureEff . runToList $ do                             Console.putStr "side effect"                             pure (42 :: Int)                 result @?= 42                 msgs @?= ["side effect"]             , testCase "traceLn appends a newline to the message" $ do-                let (_, msgs) = runPureEff $ runConsoleToList $ Console.putStrLn "line"+                let (_, msgs) = runPureEff $ runToList $ Console.putStrLn "line"                 msgs @?= ["line\n"]             ]         ]
test/Unit/Atelier/Effects/DebounceSpec.hs view
@@ -10,28 +10,20 @@ import Effectful.Concurrent.STM qualified as STM import StmContainers.Map qualified as Map -import Atelier.Effects.Conc (runConc)-import Atelier.Effects.Debounce-    ( Entry (..)-    , debounced-    , debouncedWith-    , ensureCallback-    , ensureEntry-    , runDebounce-    )-import Atelier.Effects.Delay (runDelay)+import Atelier.Effects.Debounce (Entry (..), ensureCallback, ensureEntry) import Atelier.Time (Millisecond)  import Atelier.Effects.Conc qualified as Conc import Atelier.Effects.Console qualified as Console+import Atelier.Effects.Debounce qualified as Debounce import Atelier.Effects.Delay qualified as Delay   test_Debounce :: TestTree test_Debounce =     testGroup-        "Debounce"-        [ testGroup "runDebounce" testRunDebounce+        "Atelier.Effects.Debounce"+        [ testGroup "run" testRun         , testGroup "ensureEntry" testEnsureEntry         , testGroup "ensureCallback" testEnsureCallback         ]@@ -127,9 +119,9 @@     runCallbackTest =         runEff             . runConcurrent-            . Console.runConsole-            . Delay.runDelay-            . runConc+            . Console.runIO+            . Delay.runIO+            . Conc.runIO     entry generation =         Entry             { generation@@ -143,16 +135,16 @@ -- fires. Multi-burst tests sequence by waiting for each burst's fire, so bursts -- cannot merge regardless of timing. A short grace after the fire guards against -- a spurious second fire.-testRunDebounce :: [TestTree]-testRunDebounce =+testRun :: [TestTree]+testRun =     [ testGroup-        "debounced"+        "Debounce.with"         [ testGroup             "when invoked once"             [ testCase "fires once" do                 n <- runTest do                     (count, fired) <- newCounter-                    debounced 50 () (bump count fired)+                    Debounce.with 50 () (bump count fired)                     awaitFire fired                     Delay.wait @Millisecond 80                     readCount count@@ -163,9 +155,9 @@             [ testCase "fires once" do                 n <- runTest do                     (count, fired) <- newCounter-                    debounced 50 () (bump count fired)-                    debounced 50 () (bump count fired)-                    debounced 50 () (bump count fired)+                    Debounce.with 50 () (bump count fired)+                    Debounce.with 50 () (bump count fired)+                    Debounce.with 50 () (bump count fired)                     awaitFire fired                     Delay.wait @Millisecond 80                     readCount count@@ -176,11 +168,11 @@             [ testCase "fires once per burst" do                 n <- runTest do                     (count, fired) <- newCounter-                    debounced 50 () (bump count fired)-                    debounced 50 () (bump count fired)+                    Debounce.with 50 () (bump count fired)+                    Debounce.with 50 () (bump count fired)                     awaitFire fired -- burst 1 settled and cleared the entry-                    debounced 50 () (bump count fired)-                    debounced 50 () (bump count fired)+                    Debounce.with 50 () (bump count fired)+                    Debounce.with 50 () (bump count fired)                     awaitFire fired -- burst 2 is therefore independent                     readCount count                 n @?= 2@@ -190,7 +182,7 @@             [ testCase "should not crash" do                 n <- runTest do                     (count, fired) <- newCounter-                    replicateM_ 10_000 $ debounced 50 () (bump count fired)+                    replicateM_ 10_000 $ Debounce.with 50 () (bump count fired)                     awaitFire fired                     Delay.wait @Millisecond 80                     readCount count@@ -198,13 +190,13 @@             ]         ]     , testGroup-        "debouncedWith"+        "Debounce.withArg"         [ testGroup             "when invoked once"             [ testCase "fires callback with the provided arg" do                 got <- runTest do                     result <- STM.atomically STM.newEmptyTMVar-                    debouncedWith 50 (+) () (42 :: Int) (signal result)+                    Debounce.withArg 50 (+) () (42 :: Int) (signal result)                     awaitValue result                 got @?= Just 42             ]@@ -214,9 +206,9 @@                 n <- runTest do                     (count, fired) <- newCounter                     let cb _ = bump count fired-                    debouncedWith 50 (+) () (1 :: Int) cb-                    debouncedWith 50 (+) () (2 :: Int) cb-                    debouncedWith 50 (+) () (3 :: Int) cb+                    Debounce.withArg 50 (+) () (1 :: Int) cb+                    Debounce.withArg 50 (+) () (2 :: Int) cb+                    Debounce.withArg 50 (+) () (3 :: Int) cb                     awaitFire fired                     Delay.wait @Millisecond 80                     readCount count@@ -225,9 +217,9 @@                 got <- runTest do                     result <- STM.atomically STM.newEmptyTMVar                     let cb = signal result-                    debouncedWith 50 (+) () (1 :: Int) cb-                    debouncedWith 50 (+) () (2 :: Int) cb-                    debouncedWith 50 (+) () (3 :: Int) cb+                    Debounce.withArg 50 (+) () (1 :: Int) cb+                    Debounce.withArg 50 (+) () (2 :: Int) cb+                    Debounce.withArg 50 (+) () (3 :: Int) cb                     awaitValue result                 got @?= Just 6             ]@@ -238,11 +230,11 @@                     fired <- STM.atomically STM.newEmptyTMVar                     ref <- STM.atomically (STM.newTVar @[Int] [])                     let cb v = STM.atomically (STM.modifyTVar' ref (<> [v]) >> void (STM.tryPutTMVar fired ()))-                    debouncedWith 50 (+) () (1 :: Int) cb-                    debouncedWith 50 (+) () (2 :: Int) cb+                    Debounce.withArg 50 (+) () (1 :: Int) cb+                    Debounce.withArg 50 (+) () (2 :: Int) cb                     awaitFire fired -- burst 1 fired (1+2=3); entry cleared-                    debouncedWith 50 (+) () (10 :: Int) cb-                    debouncedWith 50 (+) () (20 :: Int) cb+                    Debounce.withArg 50 (+) () (10 :: Int) cb+                    Debounce.withArg 50 (+) () (20 :: Int) cb                     awaitFire fired -- burst 2 fired (10+20=30)                     STM.atomically (STM.readTVar ref)                 xs @?= [3, 30]@@ -253,9 +245,9 @@     runTest =         runEff             . runConcurrent-            . runConc-            . runDelay-            . runDebounce+            . Conc.runIO+            . Delay.runIO+            . Debounce.run      -- A fire counter paired with a TMVar signalling that a fire happened.     newCounter = do
test/Unit/Atelier/Effects/FileSystemSpec.hs view
@@ -141,4 +141,4 @@     :: Map FilePath ByteString     -> Eff '[FileSystem, State (Map FilePath ByteString)] a     -> a-run fs = runPureEff . evalState fs . runFileSystemState+run fs = runPureEff . evalState fs . runState
test/Unit/Atelier/Effects/FileWatcherSpec.hs view
@@ -14,18 +14,18 @@ import Hedgehog.Range qualified as Range  import Atelier.Effects.FileWatcher-    ( FileEvent (..)-    , FileWatcher-    , Watch+    ( FileWatcher     , deduplicateDirs-    , dir-    , dirWhere     , matchesAny-    , runFileWatcherScripted     , watchFilePaths     )+import Atelier.Effects.FileWatcher.Event (Event (..))+import Atelier.Effects.FileWatcher.Watch (Watch) +import Atelier.Effects.FileWatcher qualified as FileWatcher+import Atelier.Effects.FileWatcher.Watch qualified as Watch + test_FileWatcher :: TestTree test_FileWatcher =     testGroup@@ -54,17 +54,17 @@         , testGroup             "matchesAny"             [ testCase "matches a file under a watched directory" do-                matchesAny [dir "/proj/src"] "/proj/src/Foo.hs"+                matchesAny [Watch.dir "/proj/src"] "/proj/src/Foo.hs"                     @?= True             , testCase "does not match a relative watch dir against an absolute event path" do                 -- runFileWatcherIO must canonicalize Watch paths to absolute before                 -- calling matchesAny, because fsnotify always reports absolute paths.-                matchesAny [dir "src"] "/proj/src/Foo.hs"+                matchesAny [Watch.dir "src"] "/proj/src/Foo.hs"                     @?= False             , testCase "applies the file predicate" do-                matchesAny [dirWhere "/proj/src" (\f -> ".hs" `isSuffixOf` f)] "/proj/src/Foo.hs"+                matchesAny [Watch.dirWhere "/proj/src" (\f -> ".hs" `isSuffixOf` f)] "/proj/src/Foo.hs"                     @?= True-                matchesAny [dirWhere "/proj/src" (\f -> ".hs" `isSuffixOf` f)] "/proj/src/Foo.js"+                matchesAny [Watch.dirWhere "/proj/src" (\f -> ".hs" `isSuffixOf` f)] "/proj/src/Foo.js"                     @?= False             ]         , testGroup "runFileWatcherScripted" testScripted@@ -86,7 +86,7 @@             paths <- collectPaths ["/src/Foo.hs", "/src/Bar.hs"]             paths @?= ["/src/Foo.hs", "/src/Bar.hs"]         , testCase "ignores the watch specification" do-            paths <- collectPathsWith [dir "/any"] ["/src/Foo.hs"]+            paths <- collectPathsWith [Watch.dir "/any"] ["/src/Foo.hs"]             paths @?= ["/src/Foo.hs"]         , testCase "passes the full path to the callback unchanged" do             let path = "/home/user/project/src/Some/Deep/Module.hs"@@ -119,7 +119,7 @@   runScripted :: [FilePath] -> Eff '[FileWatcher, Concurrent, IOE] a -> IO a-runScripted paths = runEff . runConcurrent . runFileWatcherScripted (map (,Modified) paths)+runScripted paths = runEff . runConcurrent . FileWatcher.runScripted (map (,Modified) paths)   --------------------------------------------------------------------------------
test/Unit/Atelier/Effects/IteratorSpec.hs view
@@ -6,12 +6,15 @@ import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (testCase, (@?=)) -import Atelier.Effects.Chan (Chan, runChan)-import Atelier.Effects.Clock (Clock, runClock)-import Atelier.Effects.Conc (Conc, fork, runConc)+import Atelier.Effects.Chan (Chan)+import Atelier.Effects.Clock (Clock)+import Atelier.Effects.Conc (Conc) import Atelier.Effects.PubSub.Pub (Pub) import Atelier.Effects.PubSub.Sub (Sub) +import Atelier.Effects.Chan qualified as Chan+import Atelier.Effects.Clock qualified as Clock+import Atelier.Effects.Conc qualified as Conc import Atelier.Effects.Iterator qualified as Iter import Atelier.Effects.PubSub qualified as PubSub import Atelier.Effects.PubSub.Pub qualified as Pub@@ -32,7 +35,7 @@     [ testCase "yields a published event" do         result <- runTest $ do             Iter.fromEvents @Int \iter -> do-                _ <- fork do+                _ <- Conc.fork do                     liftIO $ threadDelay 10                     Pub.publish (42 :: Int)                 Iter.next iter@@ -40,7 +43,7 @@     , testCase "yields events in publication order" do         result <- runTest $ do             Iter.fromEvents @Int \iter -> do-                _ <- fork do+                _ <- Conc.fork do                     liftIO $ threadDelay 10                     traverse_ Pub.publish [1, 2, 3]                 replicateM 3 (Iter.next iter)@@ -48,7 +51,7 @@     , testCase "buffers events so next can catch up" do         result <- runTest $ do             Iter.fromEvents @Int \iter -> do-                _ <- fork do+                _ <- Conc.fork do                     liftIO $ threadDelay 10                     traverse_ Pub.publish [1, 2, 3]                 liftIO $ threadDelay 5_000@@ -62,7 +65,7 @@     [ testCase "passes values that satisfy the predicate" do         result <- runTest $ do             Iter.fromEvents @Int \iter -> do-                _ <- fork do+                _ <- Conc.fork do                     liftIO $ threadDelay 10                     traverse_ Pub.publish [1 .. 4]                 Iter.next (Iter.filter even iter)@@ -70,7 +73,7 @@     , testCase "skips values that do not satisfy the predicate" do         result <- runTest $ do             Iter.fromEvents @Int \iter -> do-                _ <- fork do+                _ <- Conc.fork do                     liftIO $ threadDelay 10                     traverse_ Pub.publish [1 .. 6]                 replicateM 3 (Iter.next (Iter.filter even iter))@@ -83,7 +86,7 @@     [ testCase "skips values equal to the initial value" do         result <- runTest $ do             Iter.fromEvents @Int \iter -> do-                _ <- fork do+                _ <- Conc.fork do                     liftIO $ threadDelay 10                     traverse_ Pub.publish [0, 0, 1]                 Iter.next (Iter.changes 0 iter)@@ -91,7 +94,7 @@     , testCase "yields values that differ from the initial value" do         result <- runTest $ do             Iter.fromEvents @Int \iter -> do-                _ <- fork do+                _ <- Conc.fork do                     liftIO $ threadDelay 10                     traverse_ Pub.publish [1, 2, 3]                 replicateM 3 (Iter.next (Iter.changes 0 iter))@@ -99,7 +102,7 @@     , testCase "skips initial values interspersed with non-initial values" do         result <- runTest $ do             Iter.fromEvents @Int \iter -> do-                _ <- fork do+                _ <- Conc.fork do                     liftIO $ threadDelay 10                     traverse_ Pub.publish [0, 1, 0, 2, 0, 3]                 replicateM 3 (Iter.next (Iter.changes 0 iter))@@ -115,7 +118,7 @@ runTest =     runEff         . runConcurrent-        . runConc-        . runClock-        . runChan+        . Conc.runIO+        . Clock.runIO+        . Chan.run         . PubSub.run @Int
test/Unit/Atelier/Effects/LogSpec.hs view
@@ -5,14 +5,16 @@ import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (testCase, (@?=)) -import Atelier.Effects.Log (Log, Message (..), info, runLogWriter, withNamespace)+import Atelier.Effects.Log (Log, Message (..), info, withNamespace) +import Atelier.Effects.Log qualified as Log + runLogTest :: Eff [Log, Writer [Message]] a -> [Message] runLogTest =     runPureEff         . execWriter @[Message]-        . runLogWriter+        . Log.runWriter   test_Log :: TestTree@@ -61,7 +63,7 @@                             fmap (\m -> (m.namespace, m.text))                                 . runPureEff                                 . execWriter @[Message]-                                . runLogWriter+                                . Log.runWriter                                 $ do                                     info "before"                                     withNamespace "scoped" $ info "inside"@@ -76,7 +78,7 @@                             fmap (\m -> (m.namespace, m.text))                                 . runPureEff                                 . execWriter @[Message]-                                . runLogWriter+                                . Log.runWriter                                 . withNamespace "outer"                                 $ do                                     info "outer msg"
test/Unit/Atelier/Effects/PubSubSpec.hs view
@@ -7,12 +7,15 @@ import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (testCase, (@?=)) -import Atelier.Effects.Chan (Chan, runChan)-import Atelier.Effects.Clock (Clock, runClock, runClockConst)-import Atelier.Effects.Conc (Conc, runConc)+import Atelier.Effects.Chan (Chan)+import Atelier.Effects.Clock (Clock)+import Atelier.Effects.Conc (Conc) import Atelier.Effects.PubSub.Pub (Pub) import Atelier.Effects.PubSub.Sub (Sub) +import Atelier.Effects.Chan qualified as Chan+import Atelier.Effects.Clock qualified as Clock+import Atelier.Effects.Conc qualified as Conc import Atelier.Effects.PubSub qualified as PubSub import Atelier.Effects.PubSub.Pub qualified as Pub import Atelier.Effects.PubSub.Sub qualified as Sub@@ -70,9 +73,9 @@ runPubSubTest =     runEff         . runConcurrent-        . runConc-        . runClock-        . runChan+        . Conc.runIO+        . Clock.runIO+        . Chan.run         . PubSub.run @TestEvent  @@ -83,7 +86,7 @@ runPubSubTestWithClock t =     runEff         . runConcurrent-        . runConc-        . runClockConst t-        . runChan+        . Conc.runIO+        . Clock.runConst t+        . Chan.run         . PubSub.run @TestEvent
test/Unit/Atelier/Effects/TallySpec.hs view
@@ -13,13 +13,20 @@ import Hedgehog.Gen qualified as Gen import Hedgehog.Range qualified as Range -import Atelier.Effects.Clock (Clock, runClock)-import Atelier.Effects.Conc (Conc, runConc)-import Atelier.Effects.Delay (Delay, runDelay)-import Atelier.Effects.Log (Log, runLogNoOp)-import Atelier.Effects.Tally (Config (..), Tally, runTally, withTallyCheck)+import Atelier.Effects.Cache.Config (Config (..))+import Atelier.Effects.Clock (Clock)+import Atelier.Effects.Conc (Conc)+import Atelier.Effects.Delay (Delay)+import Atelier.Effects.Log (Log)+import Atelier.Effects.Tally (Tally) +import Atelier.Effects.Clock qualified as Clock+import Atelier.Effects.Conc qualified as Conc+import Atelier.Effects.Delay qualified as Delay+import Atelier.Effects.Log qualified as Log+import Atelier.Effects.Tally qualified as Tally + test_Tally :: TestTree test_Tally =     testGroup@@ -31,34 +38,17 @@                 result @?= Map.fromList [(1, 1), (42, 1), (999, 1)]             ]         , testGroup-            "Custom Classifiers"-            [ testCase "identity classifier returns the raw hit count" do-                result <- runTallyTest $ replicateM 4 (withTallyCheck @Int 1 id pure)-                result @?= [1, 2, 3, 4 :: Int]-            , testCase "multi-mark classifier yields correct mark for each hit" do-                -- Two thresholds: warning at >2, critical at >4-                let classify c-                        | c > 4 = Just True -- critical-                        | c > 2 = Just False -- warning-                        | otherwise = Nothing -- ok-                result <- runTallyTest $ replicateM 6 (withTallyCheck @Int 1 classify pure)-                result @?= [Nothing, Nothing, Just False, Just False, Just True, Just True]-            , testCase "classifier producing tuples gives both count and flag" do-                let classify c = (c, c > 2)-                result <- runTallyTest $ replicateM 4 (withTallyCheck @Int 1 classify pure)-                result @?= [(1, False), (2, False), (3, True), (4, True)]-            ]-        , testGroup             "Action Execution"             [ testCase "continuation with side effects executes correctly" do                 result <- runTallyTest $ do-                    ref <- liftIO $ IORef.newIORef ([] :: [(Int, Bool)])+                    ref <- liftIO $ IORef.newIORef ([] :: [Int])                     replicateM_ 3-                        $ withTallyCheck @Int 1 (\c -> (c, c > 2))-                        $ \(count, exceeded) ->-                            liftIO $ IORef.modifyIORef' ref ((count, exceeded) :)+                        $ Tally.count @Int+                            1+                            \count ->+                                liftIO $ IORef.modifyIORef' ref (count :)                     liftIO $ reverse <$> IORef.readIORef ref-                result @?= [(1, False), (2, False), (3, True)]+                result @?= [1, 2, 3]             ]         , testGroup             "Properties"@@ -72,11 +62,6 @@                     replicateM_ n checkCount                     checkCount                 finalCount === n + 1-            , testProperty "classifier output is passed through unchanged" $ property $ do-                n <- forAll $ Gen.int (Range.linear 1 50)-                offset <- forAll $ Gen.int (Range.linear 0 200)-                results <- liftIO $ runTallyTest $ replicateM n (withTallyCheck @Int 1 (+ offset) pure)-                results === map (+ offset) [1 .. n]             ]         ] @@ -90,23 +75,23 @@ runTallyTest action =     runEff         . runConcurrent-        . runLogNoOp-        . runConc-        . runDelay-        . runClock+        . Log.runNoOp+        . Conc.runIO+        . Delay.runIO+        . Clock.runIO         . runReader (Config {entryTtl = 3600, cleanupInterval = 3600})-        . runTally @Int+        . Tally.run @Int         $ action   -- | Increment the tally for the default test key and return the raw count checkCount :: (Tally Int :> es) => Eff es Int-checkCount = withTallyCheck @Int 1 id pure+checkCount = Tally.count @Int 1 pure   -- | Increment the tally for a specific key and return the raw count checkCountForKey :: (Hashable key, Ord key, Tally key :> es) => key -> Eff es Int-checkCountForKey key = withTallyCheck key id pure+checkCountForKey key = Tally.count key pure   -- | Increment the tally for each key and return a map of key → count
test/Unit/Atelier/Effects/YieldSpec.hs view
@@ -7,10 +7,9 @@ import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (testCase, (@?=)) -import Atelier.Effects.Chan (runChan)-import Atelier.Effects.Conc (runConc)- import Atelier.Effects.Await qualified as Await+import Atelier.Effects.Chan qualified as Chan+import Atelier.Effects.Conc qualified as Conc import Atelier.Effects.Yield qualified as Yield  @@ -115,7 +114,7 @@         xs @?= [1, 2, 3, 1, 2, 3, 1]     ]   where-    runTest = runEff . runConcurrent . runConc . runChan . execWriter+    runTest = runEff . runConcurrent . Conc.runIO . Chan.run . execWriter   testWithYieldToList :: [TestTree]
+ test/Unit/Atelier/Types/JSON/WithDefaultsSpec.hs view
@@ -0,0 +1,66 @@+module Unit.Atelier.Types.JSON.WithDefaultsSpec (test_WithDefaults) where++import Data.Aeson (FromJSON, ToJSON, eitherDecode)+import Data.Default (Default (..))+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit (testCase, (@?=))++import Data.ByteString.Lazy qualified as LBS++import Atelier.Types.JSON.QuietSnake (QuietSnake (..))+import Atelier.Types.JSON.WithDefaults (WithDefaults (..))+++-- | A minimal fixture type with non-Maybe list fields to exercise WithDefaults.+data Fixture = Fixture+    { requiredField :: Text+    , items :: [Text]+    , count :: Int+    }+    deriving stock (Eq, Generic, Show)+    deriving (FromJSON, ToJSON) via QuietSnake Fixture+++instance Default Fixture where+    def =+        Fixture+            { requiredField = "default-value"+            , items = ["default-item"]+            , count = 0+            }+++decodeWithDefaults :: LBS.ByteString -> Either String Fixture+decodeWithDefaults bs = getQuietSnake . getWithDefaults <$> eitherDecode @(WithDefaults (QuietSnake Fixture)) bs+++test_WithDefaults :: TestTree+test_WithDefaults =+    testGroup+        "WithDefaults"+        [ testGroup+            "WithDefaults"+            [ testCase "falls back to Default values for missing non-Maybe fields" do+                let result = decodeWithDefaults "{}"+                result @?= Right def+            , testCase "uses provided values when all fields are present" do+                let result =+                        decodeWithDefaults+                            "{\"required_field\": \"hello\", \"items\": [\"a\", \"b\"], \"count\": 42}"+                result+                    @?= Right+                        Fixture+                            { requiredField = "hello"+                            , items = ["a", "b"]+                            , count = 42+                            }+            , testCase "partial object: provided fields override defaults, missing fall back" do+                let result = decodeWithDefaults "{\"count\": 7}"+                result+                    @?= Right+                        def {count = 7}+            , testCase "explicitly provided empty list overrides the default list" do+                let result = decodeWithDefaults "{\"items\": []}"+                result @?= Right def {items = []}+            ]+        ]
test/Unit/Atelier/Types/Semaphore/STMSpec.hs view
@@ -20,7 +20,7 @@         [ testGroup             "wait"             [ testCase "should halt the computation" do-                runEff . runConcurrent . Conc.runConc . evalState @Int 0 $ do+                runEff . runConcurrent . Conc.runIO . evalState @Int 0 $ do                     sem <- atomically Sem.new                      thread <- Conc.fork do@@ -136,7 +136,7 @@                     atomically $ Sem.peek sem                 result @?= True             , testCase "waits for the semaphore before running" do-                result <- runEff . runConcurrent . Conc.runConc $ do+                result <- runEff . runConcurrent . Conc.runIO $ do                     sem <- atomically Sem.new                     ref <- liftIO $ IORef.newIORef False                     _ <- Conc.fork $ do
test/Unit/Atelier/Types/SemaphoreSpec.hs view
@@ -19,7 +19,7 @@         [ testGroup             "wait"             [ testCase "should halt the computation" do-                runEff . runConcurrent . Conc.runConc . evalState @Int 0 $ do+                runEff . runConcurrent . Conc.runIO . evalState @Int 0 $ do                     sem <- Sem.new                      thread <- Conc.fork do@@ -135,7 +135,7 @@                     Sem.peek sem                 result @?= True             , testCase "waits for the semaphore before running" do-                result <- runEff . runConcurrent . Conc.runConc $ do+                result <- runEff . runConcurrent . Conc.runIO $ do                     sem <- Sem.new                     ref <- liftIO $ IORef.newIORef False                     _ <- Conc.fork $ do
− test/Unit/Atelier/Types/WithDefaultsSpec.hs
@@ -1,66 +0,0 @@-module Unit.Atelier.Types.WithDefaultsSpec (test_WithDefaults) where--import Data.Aeson (FromJSON, ToJSON, eitherDecode)-import Data.Default (Default (..))-import Test.Tasty (TestTree, testGroup)-import Test.Tasty.HUnit (testCase, (@?=))--import Data.ByteString.Lazy qualified as LBS--import Atelier.Types.JSON.QuietSnake (QuietSnake (..))-import Atelier.Types.WithDefaults (WithDefaults (..))----- | A minimal fixture type with non-Maybe list fields to exercise WithDefaults.-data Fixture = Fixture-    { requiredField :: Text-    , items :: [Text]-    , count :: Int-    }-    deriving stock (Eq, Generic, Show)-    deriving (FromJSON, ToJSON) via QuietSnake Fixture---instance Default Fixture where-    def =-        Fixture-            { requiredField = "default-value"-            , items = ["default-item"]-            , count = 0-            }---decodeWithDefaults :: LBS.ByteString -> Either String Fixture-decodeWithDefaults bs = getQuietSnake . getWithDefaults <$> eitherDecode @(WithDefaults (QuietSnake Fixture)) bs---test_WithDefaults :: TestTree-test_WithDefaults =-    testGroup-        "WithDefaults"-        [ testGroup-            "WithDefaults"-            [ testCase "falls back to Default values for missing non-Maybe fields" do-                let result = decodeWithDefaults "{}"-                result @?= Right def-            , testCase "uses provided values when all fields are present" do-                let result =-                        decodeWithDefaults-                            "{\"required_field\": \"hello\", \"items\": [\"a\", \"b\"], \"count\": 42}"-                result-                    @?= Right-                        Fixture-                            { requiredField = "hello"-                            , items = ["a", "b"]-                            , count = 42-                            }-            , testCase "partial object: provided fields override defaults, missing fall back" do-                let result = decodeWithDefaults "{\"count\": 7}"-                result-                    @?= Right-                        def {count = 7}-            , testCase "explicitly provided empty list overrides the default list" do-                let result = decodeWithDefaults "{\"items\": []}"-                result @?= Right def {items = []}-            ]-        ]