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! }