mole-0.0.5: 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 qualified Data.Set as S
import Data.Time (UTCTime, NominalDiffTime)
import Data.Text (Text)
import qualified Data.Text as T
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 :: Text }
deriving (Ord, Eq)
instance Show AssetId where
show = T.unpack . unAssetId
newtype BuildId = BuildId { unBuildId :: Int }
deriving (Ord, Eq, Show)
type ContentType = String
data AssetState
= Dirty
-- ^ The asset is marked as dirty and needs to be built when some other
-- asset depends on it.
| Building !UTCTime
-- ^ A thread is currently building the asset. The timestamp is the time
-- when the thread was forked. It is used to keep track how long it took
-- to build the asset.
| Failed Error
| Completed !NominalDiffTime
-- ^ The asset is available. The time is how long it took to build it.
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
-- ^ This is a approximation of the set of files which make up the asset.
-- When any of these files change, the asset is marked as dirty.
, arsDependencySet :: Set AssetId
-- ^ Assets which are depended on. These must be built before this asset
-- can be further processed. Also, if any of the dependencies changes,
-- this asset is rebuilt as well.
, arsSource :: Maybe (ByteString, Map AssetId PublicIdentifier)
-- ^ This is used to avoid processing the file when its contents have
-- not changed. The first element of the tuple is the file contents or
-- fingerprint (hash), the second element are the resolved dependencies.
-- If the two are same, then the result SHOULD as well.
, arsResult :: Maybe Result
-- ^ If the asset is built, this is the result. This can be set even
-- if the state is not 'Completed'. This is the case when the asset
-- is being rebuilt. If the state is not 'Completed' you should wait for
-- it to reach that state before using the result, otherwise you may
-- get a stale result.
} deriving (Show)
-- | The initial, empty 'AssetRuntimeState', with the given 'AssetState'
-- (usually 'Dirty' or 'Building' depending at which stage it is created).
assetRuntimeState :: AssetState -> AssetRuntimeState
assetRuntimeState s = AssetRuntimeState
{ arsState = s
, arsSources = S.empty
, arsDependencySet = S.empty
, arsSource = Nothing
, arsResult = Nothing
}
-- | This is how one can refer to processed assets. Probably should be a newtype
-- around 'URL', because that's what those really are (well, just the path part
-- of an URL).
newtype PublicIdentifier = PublicIdentifier { unPublicIdentifier :: Text }
deriving (Eq, Show)
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.
, sourceFingerprint :: ByteString
-- ^ Fingerprint of all sources which make up the asset. This can be the
-- hash of all the input files, or the contents themselves (if not too
-- large). This is used to avoid rebuilding the asset if the input hasn't
-- actually changed. Often only the atime changes, but that has
-- no relevance on the output.
}
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!
}