mole-0.0.3: src/Data/Mole/Types.hs
module Data.Mole.Types where
import Control.Concurrent
import Control.Concurrent.STM
import Data.ByteString (ByteString)
import Data.Map (Map)
import Data.Set (Set)
import Data.Time (UTCTime)
import qualified Network.Kraken as K
-- | An 'AssetId' is an internal reference to an unprocessed asset. Source files
-- use those to express dependencies on other files. And the 'Config' object
-- contains definitions how to build those.
--
-- Even though the 'AssetId' may look like a filename or path, it doesn't have
-- to refer to an actual file in the filesystem.
newtype AssetId = AssetId { unAssetId :: String }
deriving (Ord, Eq)
instance Show AssetId where
show = unAssetId
newtype BuildId = BuildId { unBuildId :: Int }
deriving (Ord, Eq, Show)
type ContentType = String
data AssetState = Dirty | Building | Failed Error | Completed Result
deriving (Eq, Show)
data Config = Config
{ assetDefinitions :: Map AssetId AssetDefinition
-- ^ All the assets we know how to build. This doesn't mean that they
-- actually will be built. Only if they are referenced / required /
-- reachable through one of the entry points.
--
-- This list is rarely created manually. Usually it's automatically
-- generated by traversing a source directory and converting each
-- file into an asset (depending on the file type).
--
-- Another option is to only define the entry points and maybe a few
-- special assets and let auto-discovery do the rest.
, autoDiscovery :: Handle -> AssetId -> IO (Maybe AssetDefinition)
-- ^ If an asset is not defined statically, we attempt to do auto-discovery
-- based on its 'AssetId'. The default implementation tries to locate the
-- file below the base path where all the sources are. This works really
-- well for binary files which need no processing (eg. images, font files)
-- or files where we can infer the 'AssetDefinition' from its content type
-- or file extension.
, entryPoints :: [AssetId]
-- ^ The entry points into the application. Usually this will include at
-- least the index file (eg. index.html).
}
data AssetRuntimeState = AssetRuntimeState
{ arsState :: AssetState
, arsSources :: Set FilePath
, arsDependencySet :: Set AssetId
} deriving (Show)
type PublicIdentifier = String
data Result = Result
{ publicIdentifier :: PublicIdentifier
-- ^ This is how other parts of the application can refer to the asset.
-- This can be an absolute path or a full URL (for example if you're
-- serving the assets from a CDN).
, resource :: Maybe (ByteString, ContentType)
-- ^ The content of the asset if built locally. For external assets (eg.
-- jquery served from the google CDN) this is 'Nothing'.
} deriving (Eq, Show)
data Error
= UndeclaredDependency AssetId
| AssetNotFound AssetId
| DependencyFailed
deriving (Show, Eq)
data Builder = Builder
{ assetSources :: Set FilePath
-- An approximation of files which contributed to the asset. This is
-- stored in the state and used to trigger rebuilds when the files on
-- disk change.
, assetDependencies :: Set AssetId
-- ^ The dependencies of the asset which is currently being built. These
-- dependencies are automatically built before the asset is packaged
-- into its final result.
, packageAsset :: Map AssetId PublicIdentifier -> Either Error Result
-- ^ A function which takes the public identifiers for all dependencies
-- and creates the final asset package.
}
data AssetDefinition = AssetDefinition
{ createBuilder :: Handle -> AssetId -> IO Builder
-- ^ IO action which returns metadata about the asset and a function
-- which assembles the asset into its final form.
, transformPublicIdentifier :: PublicIdentifier -> PublicIdentifier
-- ^ An optional transformer for the 'PublicIdentifier'. Use this if you
-- want to serve the assets from a different path or domain. The default
-- implementation simply prepends "/" to the pubId, therefore making the
-- path absolute. If you want to serve the assets from a subdirectory,
-- prepend for example "/assets/". If you use a CDN, make the pubId a
-- full URL.
, emitResult :: Handle -> AssetId -> Result -> IO ()
-- ^ Action which is invoked every time an asset has completed building.
-- This is useful if you want to store the asset in an output directory
-- or maybe even directly upload to the server.
}
data State = State
{ dispatcherThreadId :: Maybe ThreadId
-- ^ The thread which waits for assets to be marked as 'Dirty' and
-- dispatches build jobs. Doing that in a single thread makes it easier
-- to synchronize STM with IO.
, stopFileWatcher :: IO ()
-- ^ The file watcher is run in a separate thread. This is the action
-- to stop it.
, assets :: Map AssetId AssetRuntimeState
}
data Message = Message UTCTime AssetId String
data Handle = Handle
{ state :: TVar State
, messages :: TQueue Message
, emitStream :: TQueue (IO ())
-- ^ This is used to serialize the emit actions. This is required because
-- two different AssetIds may map to the same PublicIdentifier and
-- Haskell throws an exception when writing to the same file concurrently.
, krakenH :: !(Maybe K.Handle)
--, messageThreadId :: ThreadId
, lock :: !(TMVar ())
-- ^ Generic lock for various things. Please make sure to not deadlock!
}