----------------------------------------------------------------------------- {-# LANGUAGE CPP #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE OverloadedStrings #-} ----------------------------------------------------------------------------- -- | -- Module : Miso.Reload -- Copyright : (C) 2016-2026 David M. Johnson -- License : BSD3-style (see the file LICENSE) -- Maintainer : David M. Johnson <code@dmj.io> -- Stability : experimental -- Portability : non-portable -- -- Support for live reloading of miso applications. -- -- = Live Reload -- -- This module exposes two functions meant to be used during interactive -- development with GHC WASM browser mode, 'live' and 'reload'. -- -- == Reload -- -- Use 'reload' if you'd like to redraw the page on each file change, resetting -- the working application state. -- -- @ -- main :: IO () -- main = 'reload' 'defaultEvents' app -- @ -- -- == Live -- -- Use 'live' if you'd like to persist the working application state (all 'Component' 'model') -- between GHCi reloads. This only works if you do not alter the 'model' schema (e.g. add, remove, change a field's type). -- -- @ -- main :: IO () -- main = 'live' 'defaultEvents' app -- @ -- -- See the [miso-sampler](https://github.com/haskell-miso/miso-sampler) for example usage. -- ---------------------------------------------------------------------------- module Miso.Reload ( -- ** Functions reload , live ) where ----------------------------------------------------------------------------- import Control.Concurrent import Control.Monad ----------------------------------------------------------------------------- import Miso.DSL ((!), jsg, setField) import qualified Miso.FFI.Internal as FFI import Miso.Types (Component(..), Events, App) import Miso.String (MisoString) import Miso.Runtime (componentModel, initComponent, topLevelComponentId, Hydrate(..)) import Miso.Runtime.Internal (components, schedulerThread) ----------------------------------------------------------------------------- import Miso.Lens ----------------------------------------------------------------------------- import qualified Data.IntMap.Strict as IM import Data.IORef import Foreign hiding (void) import Foreign.C.Types ----------------------------------------------------------------------------- foreign import ccall unsafe "miso_x_store" x_store :: StablePtr a -> IO () ----------------------------------------------------------------------------- foreign import ccall unsafe "miso_x_get" x_get :: IO (StablePtr a) ----------------------------------------------------------------------------- foreign import ccall unsafe "miso_x_exists" x_exists :: IO CInt ----------------------------------------------------------------------------- foreign import ccall unsafe "miso_x_clear" x_clear :: IO () ----------------------------------------------------------------------------- #define MISO_JS_PATH "js/miso.js" ----------------------------------------------------------------------------- -- | Clears the \<body\> and \<head\> on each 'reload'. -- -- Meant to be used with WASM browser mode. -- -- @ -- main :: IO () -- main = 'reload' 'defaultEvents' app -- @ -- -- N.B. This also resets the internal 'component' state. This means all currently -- mounted components become unmounted and t'ComponentId' are reset to their -- original form factory. -- -- If you'd like to preserve application state between calls to GHCi `:r`, see 'live'. -- -- @since 1.9.0.0 reload :: Eq model => Events -> App model action -> IO () reload :: forall model action. Eq model => Events -> App model action -> IO () reload Events events App model action vcomp = do exists <- IO CInt x_exists when (exists == 1) $ do (_, oldSchedulerRef) <- deRefStablePtr =<< x_get killThread =<< readIORef oldSchedulerRef x_clear clearPage void (initComponent events Draw False vcomp) x_store =<< newStablePtr (components, schedulerThread) ----------------------------------------------------------------------------- -- | Live reloading. Persists all t'Component' `model` between successive GHCi reloads. -- -- This means application state should persist between GHCi reloads -- -- Schema changes to 'model' are currently unsupported. If you're -- changing fields in 'model' (adding, removing, changing a field's type), this -- will more than likely segfault. If you change the 'view' or 'update' functions -- it will be fine. -- -- Use 'reload' if you're changing the 'model' frequently and 'live' -- if you're adjusting the 'view' / 'update' function logic. -- -- @ -- main :: IO () -- main = 'live' 'defaultEvents' app -- @ -- -- @since 1.9.0.0 live :: Eq model => Events -> App model action -> IO () live :: forall model action. Eq model => Events -> App model action -> IO () live Events events App model action vcomp = do exists <- IO CInt x_exists if exists == 1 then do -- clearBody (only clear the body) clearBody -- Deref old state, update new state, set pointer in C heap. (oldComponentsRef, oldSchedulerRef) <- deRefStablePtr =<< x_get killThread =<< readIORef oldSchedulerRef _oldState <- readIORef oldComponentsRef let oldModel = (IntMap (ComponentState (ZonkAny 0) (ZonkAny 1) model (ZonkAny 2)) _oldState IntMap (ComponentState (ZonkAny 0) (ZonkAny 1) model (ZonkAny 2)) -> Key -> ComponentState (ZonkAny 0) (ZonkAny 1) model (ZonkAny 2) forall a. IntMap a -> Key -> a IM.! Key topLevelComponentId) ComponentState (ZonkAny 0) (ZonkAny 1) model (ZonkAny 2) -> Lens (ComponentState (ZonkAny 0) (ZonkAny 1) model (ZonkAny 2)) model -> model forall record field. record -> Lens record field -> field ^. Lens (ComponentState (ZonkAny 0) (ZonkAny 1) model (ZonkAny 2)) model forall parent props model action. Lens (ComponentState parent props model action) model componentModel initialVComp = App model action vcomp { model = oldModel } -- Overwrite new components state with old components state atomicWriteIORef components _oldState -- Perform initial draw, this will fetch the model from the old component state -- and overwrite the old state with the new state for everything else. initComponent events Draw True initialVComp -- Don't forget to flush (native mobile needs this too) FFI.flush -- Clear and set static ptr to use new state (new CAF state) x_clear x_store =<< newStablePtr (components, schedulerThread) else do -- This means it is initial load, just store the pointer. void (initComponent events Draw False vcomp) x_store =<< newStablePtr (components, schedulerThread) ----------------------------------------------------------------------------- clearPage, clearBody, clearHead :: IO () clearPage :: IO () clearPage = IO () clearBody IO () -> IO () -> IO () forall a b. IO a -> IO b -> IO b forall (m :: * -> *) a b. Monad m => m a -> m b -> m b >> IO () clearHead clearBody :: IO () clearBody = do body_ <- MisoString -> IO JSVal jsg MisoString "document" IO JSVal -> MisoString -> IO JSVal forall o. ToObject o => o -> MisoString -> IO JSVal ! (MisoString "body" :: MisoString) setField body_ "innerHTML" ("" :: MisoString) clearHead :: IO () clearHead = do head_ <- MisoString -> IO JSVal jsg MisoString "document" IO JSVal -> MisoString -> IO JSVal forall o. ToObject o => o -> MisoString -> IO JSVal ! (MisoString "head" :: MisoString) setField head_ "innerHTML" ("" :: MisoString) -----------------------------------------------------------------------------