on master help sign in
Merge pull request #366 from obsidiansystems/hydration

Hydration

commit258797ed41564512fb1e946d8be630bb86482afe authorAli Abrar <aliabrar@gmail.com> dateWed 13 Mar 2019, 12:18 (8 years ago) parent1eeff45b153452
11 files changed+95−95
dep/reflex-platform/github.json+3−3 lib/backend/obelisk-backend.cabal−1 lib/backend/src/Obelisk/Backend.hs+2−4 lib/command/src/Obelisk/Command/Run.hs+1−1 lib/frontend/obelisk-frontend.cabal+2 lib/frontend/src/Obelisk/Frontend.hs+38−70 lib/route/src/Obelisk/Route.hs+20 lib/route/src/Obelisk/Route/Frontend.hs+25−12 lib/run/obelisk-run.cabal−1 lib/run/src/Obelisk/Run.hs+1−2 skeleton/common/common.cabal+3−1
dep/reflex-platform/github.json+3−3
@@ -1,7 +1,7 @@
11{
22 "owner": "reflex-frp",
33 "repo": "reflex-platform",
4− "branch": "develop",
5− "rev": "a7cd9a23e7faa9c2545a4c111229e28208e42351",
6− "sha256": "0y52rfrhk4zszgprpyni51l0pgq18dg695k5bmpd62c3zxar5mvm"
4+ "branch": "hydration",
5+ "rev": "d4b1dcfa6d2795e612b9a4d89c40467afb9fad9d",
6+ "sha256": "0wqcvmsivhk9h30p8fmwwp10qn8l8mwfxishpxpxwprc1kh4dbc4"
77}
lib/backend/obelisk-backend.cabal−1
@@ -14,7 +14,6 @@ library
1414 lens,
1515 mtl,
1616 obelisk-asset-serve-snap,
17− obelisk-executable-config-inject,
1817 obelisk-frontend,
1918 obelisk-route,
2019 obelisk-snap-extras,
lib/backend/src/Obelisk/Backend.hs+2−4
@@ -37,7 +37,6 @@ import Data.Text (Text)
3737import qualified Data.Text as T
3838import Data.Text.Encoding
3939import Obelisk.Asset.Serve.Snap (serveAsset)
40−import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
4140import Obelisk.Frontend
4241import Obelisk.Route
4342import Obelisk.Snap.Extras (doNotCache, serveFileIfExistsAs)
@@ -167,9 +166,8 @@ mkRouteToUrl validFullEncoder =
167166
168167renderGhcjsFrontend :: MonadIO m => (route -> Text) -> route -> Frontend route -> m ByteString
169168renderGhcjsFrontend urlEnc route f = do
170− let baseTag = elAttr "base" ("href" =: "/") blank --TODO: Figure out the base URL from the routes
171− ghcjsPreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "ghcjs/all.js") blank
169+ let ghcjsPreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "ghcjs/all.js") blank
172170 ghcjsScript = elAttr "script" ("language" =: "javascript" <> "src" =: "ghcjs/all.js" <> "defer" =: "defer") blank
173171 liftIO $ renderFrontendHtml urlEnc route
174− (_frontend_head f >> injectExecutableConfigs >> baseTag >> ghcjsPreload)
172+ (_frontend_head f >> ghcjsPreload)
175173 (_frontend_body f >> ghcjsScript)
lib/command/src/Obelisk/Command/Run.hs+1−1
@@ -76,7 +76,7 @@ run = do
7676 proc "nix" ["eval", "-f", root, "passthru.staticFilesImpure", "--raw"]
7777 putLog Debug $ "Assets impurely loaded from: " <> assets
7878 runGhcid dotGhciPath $ Just $ unwords
79− [ "run"
79+ [ "Obelisk.Run.run"
8080 , show freePort
8181 , "(runServeAsset " ++ show assets ++ ")"
8282 , "Backend.backend"
lib/frontend/obelisk-frontend.cabal+2
@@ -11,6 +11,8 @@ library
1111 ghcjs-dom,
1212 jsaddle,
1313 lens,
14+ mtl,
15+ obelisk-executable-config-inject,
1416 obelisk-route,
1517 primitive,
1618 ref-tf,
lib/frontend/src/Obelisk/Frontend.hs+38−70
@@ -5,6 +5,7 @@
55{-# LANGUAGE MultiParamTypeClasses #-}
66{-# LANGUAGE OverloadedStrings #-}
77{-# LANGUAGE RankNTypes #-}
8+{-# LANGUAGE RecursiveDo #-}
89{-# LANGUAGE ScopedTypeVariables #-}
910{-# LANGUAGE TemplateHaskell #-}
1011{-# LANGUAGE TypeFamilies #-}
@@ -21,18 +22,22 @@ import Prelude hiding ((.))
2122import Control.Category
2223import Control.Concurrent
2324import Control.Lens
24−import Control.Monad
25+import Control.Monad hiding (sequence, sequence_)
2526import Control.Monad.Fix
2627import Control.Monad.IO.Class
2728import Control.Monad.Primitive
29+import Control.Monad.Reader
2830import Control.Monad.Ref
2931import Control.Monad.Trans.Class
3032import Data.ByteString (ByteString)
3133import Data.Dependent.Sum (DSum (..))
34+import Data.Foldable (for_)
3235import Data.Functor.Sum
3336import Data.IORef
3437import Data.Monoid ((<>))
3538import Data.Text (Text)
39+import Data.Foldable (sequence_)
40+import Data.Traversable (sequence)
3641import GHCJS.DOM hiding (bracket, catch)
3742import GHCJS.DOM.Document
3843import GHCJS.DOM.Node
@@ -42,10 +47,11 @@ import Obelisk.Route.Frontend
4247import Reflex.Dom.Core
4348import Reflex.Host.Class
4449import qualified Reflex.TriggerEvent.Base as TriggerEvent
50+import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
4551
4652makePrisms ''Sum
4753
48−type ObeliskWidget t x route m =
54+type ObeliskWidget js t route m =
4955 ( DomBuilder t m
5056 , MonadFix m
5157 , MonadHold t m
@@ -53,8 +59,6 @@ type ObeliskWidget t x route m =
5359 , MonadReflexCreateTrigger t m
5460 , PostBuild t m
5561 , PerformEvent t m
56− , MonadIO m
57− , MonadIO (Performable m)
5862 , TriggerEvent t m
5963 , HasDocument m
6064 , MonadRef m
@@ -63,73 +67,25 @@ type ObeliskWidget t x route m =
6367 , Ref (Performable m) ~ Ref IO
6468 , MonadFix (Performable m)
6569 , PrimMonad m
66− , Prerender x m
67− , SetRoute t route m
70+ , Prerender js t m
71+ , PrebuildAgnostic t route m
72+ , PrebuildAgnostic t route (Client m)
73+ -- TODO Remove these. Probably requires a new class to allow executable-configs to work without being inside a `prerender`
74+ , MonadIO m
75+ , MonadIO (Performable m)
76+ )
77+
78+type PrebuildAgnostic t route m =
79+ ( SetRoute t route m
6880 , RouteToUrl route m
81+ , MonadFix m
6982 )
7083
7184data Frontend route = Frontend
72− { _frontend_head :: !(forall t m x. ObeliskWidget t x route m => RoutedT t route m ())
73− , _frontend_body :: !(forall t m x. ObeliskWidget t x route m => RoutedT t route m ())
85+ { _frontend_head :: !(forall js t m. ObeliskWidget js t route m => RoutedT t route m ())
86+ , _frontend_body :: !(forall js t m. ObeliskWidget js t route m => RoutedT t route m ())
7487 }
7588
76−type Widget' x = ImmediateDomBuilderT DomTimeline (DomCoreWidget x)
77−
78−-- | A widget that isn't attached to any particular part of the DOM hierarchy
79−type FloatingWidget x = TriggerEventT DomTimeline (DomCoreWidget x)
80−
81−type DomCoreWidget x = PostBuildT DomTimeline (WithJSContextSingleton x (PerformEventT DomTimeline DomHost))
82−
83−--TODO: Rename
84−{-# INLINABLE attachWidget''' #-}
85−attachWidget'''
86− :: (EventChannel -> PerformEventT DomTimeline DomHost (IORef (Maybe (EventTrigger DomTimeline ()))))
87− -> IO ()
88−attachWidget''' w = runDomHost $ do
89− events <- liftIO newChan
90− (postBuildTriggerRef, fc@(FireCommand fire)) <- hostPerformEventT $ w events
91− mPostBuildTrigger <- readRef postBuildTriggerRef
92− forM_ mPostBuildTrigger $ \postBuildTrigger -> fire [postBuildTrigger :=> Identity ()] $ return ()
93− liftIO $ processAsyncEvents events fc
94−
95−--TODO: This is a collection of random stuff; we should make it make some more sense and then upstream to reflex-dom-core
96−runWithHeadAndBody
97− :: ( (forall c. Widget' () c -> FloatingWidget () c) -- "Append to head"
98− -> (forall c. Widget' () c -> FloatingWidget () c) -- "Append to body"
99− -> FloatingWidget () ()
100− )
101− -> JSM ()
102−runWithHeadAndBody app = withJSContextSingletonMono $ \jsSing -> do
103− globalDoc <- currentDocumentUnchecked
104− headFragment <- createDocumentFragment globalDoc
105− bodyFragment <- createDocumentFragment globalDoc
106− unreadyChildren <- liftIO $ newIORef 0
107− let commit = do
108− headElement <- getHeadUnchecked globalDoc
109− bodyElement <- getBodyUnchecked globalDoc
110− void $ inAnimationFrame' $ \_ -> do
111− replaceElementContents headElement headFragment
112− replaceElementContents bodyElement bodyFragment
113− liftIO $ attachWidget''' $ \events -> flip runWithJSContextSingleton jsSing $ do
114− (postBuild, postBuildTriggerRef) <- newEventWithTriggerRef
115− let appendImmediateDom :: DOM.DocumentFragment -> Widget' () c -> FloatingWidget () c
116− appendImmediateDom df w = do
117− events' <- TriggerEvent.askEvents
118− lift $ do
119− doc <- getOwnerDocumentUnchecked df
120− let builderEnv = ImmediateDomBuilderEnv
121− { _immediateDomBuilderEnv_document = doc
122− , _immediateDomBuilderEnv_parent = toNode df
123− , _immediateDomBuilderEnv_unreadyChildren = unreadyChildren
124− , _immediateDomBuilderEnv_commitAction = commit
125− }
126− runImmediateDomBuilderT w builderEnv events'
127− flip runPostBuildT postBuild $ flip runTriggerEventT events $ app (appendImmediateDom headFragment) (appendImmediateDom bodyFragment)
128− liftIO (readIORef unreadyChildren) >>= \case
129− 0 -> DOM.liftJSM commit
130− _ -> return ()
131− return postBuildTriggerRef
132−
13389runFrontend :: forall backendRoute route. Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName -> Frontend (R route) -> JSM ()
13490runFrontend validFullEncoder frontend = do
13591 let ve = validFullEncoder . hoistParse errorLeft (prismEncoder (rPrism $ _InR . _ObeliskRoute_App))
@@ -145,12 +101,21 @@ runFrontend validFullEncoder frontend = do
145101 , MonadFix m
146102 , MonadFix (Performable m)
147103 )
148− => RoutedT t (R route) (SetRouteT t (R route) (RouteToUrlT (R route) m)) a
104+ => Event t ()
105+ -> RoutedT t (R route) (SetRouteT t (R route) (RouteToUrlT (R route) m)) a
149106 -> m a
150107 runMyRouteViewT = runRouteViewT ve
151− runWithHeadAndBody $ \appendHead appendBody -> runMyRouteViewT $ do
152− mapRoutedT (mapSetRouteT (mapRouteToUrlT appendHead)) $ _frontend_head frontend
153− mapRoutedT (mapSetRouteT (mapRouteToUrlT appendBody)) $ _frontend_body frontend
108+ runHydrationWidgetWithHeadAndBody (pure ()) $ \appendHead appendBody -> do
109+ rec switchover <- runMyRouteViewT switchover $ do
110+ (switchover, fire) <- newTriggerEvent
111+ mapRoutedT (mapSetRouteT (mapRouteToUrlT appendHead)) $ do
112+ _frontend_head frontend
113+ mapRoutedT (mapSetRouteT (mapRouteToUrlT appendBody)) $ do
114+ _frontend_body frontend
115+ switchover' <- lift $ lift $ lift $ HydrationDomBuilderT $ asks _hydrationDomBuilderEnv_switchover
116+ performEvent_ $ liftIO (fire ()) <$ switchover'
117+ pure switchover
118+ pure ()
154119
155120renderFrontendHtml
156121 :: (t ~ DomTimeline)
@@ -163,6 +128,9 @@ renderFrontendHtml urlEnc route headWidget bodyWidget = do
163128 --TODO: We should probably have a "NullEventWriterT" or a frozen reflex timeline
164129 html <- fmap snd $ renderStatic $ fmap fst $ flip runRouteToUrlT urlEnc $ runSetRouteT $ flip runRoutedT (pure route) $
165130 el "html" $ do
166− el "head" headWidget
131+ el "head" $ do
132+ let baseTag = elAttr "base" ("href" =: "/") blank --TODO: Figure out the base URL from the routes
133+ -- The order here is important - baseTag has to be before headWidget!
134+ baseTag >> injectExecutableConfigs >> headWidget
167135 el "body" bodyWidget
168136 return $ "<!DOCTYPE html>" <> html
lib/route/src/Obelisk/Route.hs+20
@@ -82,6 +82,9 @@ module Obelisk.Route
8282 , renderObeliskRoute
8383 , renderBackendRoute
8484 , renderFrontendRoute
85+ , readShowEncoder
86+ , integralEncoder
87+ , pathSegmentEncoder
8588 ) where
8689
8790import Prelude hiding ((.), id)
@@ -118,6 +121,7 @@ import qualified Data.Text as T
118121import Data.Text.Encoding
119122import Data.Universe
120123import Network.HTTP.Types.URI
124+import qualified Numeric.Lens
121125import Obelisk.Route.TH
122126import Text.Read (readMaybe)
123127
@@ -859,4 +863,20 @@ renderObeliskRoute e r =
859863 enc = (pageNameEncoder . hoistParse (pure . runIdentity) e)
860864 in (T.pack . uncurry (<>)) $ encode enc r
861865
866+readShowEncoder :: (MonadError Text parse, Read a, Show a, Applicative check) => Encoder check parse a PageName
867+readShowEncoder = singlePathSegmentEncoder . unsafeTshowEncoder
868+
869+
870+integralEncoder :: (MonadError Text parse, Applicative check, Integral a) => Encoder check parse a Integer
871+integralEncoder = prismEncoder (Numeric.Lens.integral)
872+
873+pathSegmentEncoder :: (MonadError Text parse, Applicative check) =>
874+ Encoder check parse (Text, PageName) PageName
875+pathSegmentEncoder = unsafeMkEncoder EncoderImpl
876+ { _encoderImpl_encode = \(x, (y, z)) -> (x:y, z)
877+ , _encoderImpl_decode = \(xss, y) -> case xss of
878+ [] -> throwError "not enough path segments"
879+ (x:xs) -> pure (x, (xs, y))
880+ }
881+
862882--TODO: decodeURIComponent as appropriate
lib/route/src/Obelisk/Route/Frontend.hs+25−12
@@ -14,6 +14,7 @@
1414{-# LANGUAGE RankNTypes #-}
1515{-# LANGUAGE ScopedTypeVariables #-}
1616{-# LANGUAGE TemplateHaskell #-}
17+{-# LANGUAGE TupleSections #-}
1718{-# LANGUAGE TypeFamilies #-}
1819{-# LANGUAGE TypeOperators #-}
1920{-# LANGUAGE ViewPatterns #-}
@@ -62,7 +63,6 @@ import Control.Monad.Trans
6263import Control.Monad.Trans.Control
6364import Control.Monad.Trans.Reader
6465import Data.Coerce
65−import Data.Constraint (Dict (..))
6666import Data.Dependent.Sum (DSum (..))
6767import Data.GADT.Compare
6868import Data.Monoid
@@ -102,14 +102,17 @@ instance Monad m => Routed t r (RoutedT t r m) where
102102 askRoute = RoutedT ask
103103
104104newtype RoutedT t r m a = RoutedT { unRoutedT :: ReaderT (Dynamic t r) m a }
105− deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, MonadIO, MonadReflexCreateTrigger t, HasDocument)
105+ deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, MonadIO, MonadReflexCreateTrigger t, HasDocument, DomRenderHook t)
106106
107107instance HasJSContext m => HasJSContext (RoutedT t r m) where
108108 type JSContextPhantom (RoutedT t r m) = JSContextPhantom m
109109 askJSContext = lift askJSContext
110110
111−instance Prerender js m => Prerender js (RoutedT t r m) where
112− prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
111+instance (Prerender js t m, Monad m) => Prerender js t (RoutedT t r m) where
112+ type Client (RoutedT t r m) = RoutedT t r (Client m)
113+ prerender server client = RoutedT $ do
114+ r <- ask
115+ lift $ prerender (runRoutedT server r) (runRoutedT client r)
113116
114117instance Requester t m => Requester t (RoutedT t r m) where
115118 type Request (RoutedT t r m) = Request m
@@ -231,7 +234,7 @@ strictDynWidget_ f = RoutedT $ ReaderT $ \r -> do
231234 pure ()
232235
233236newtype SetRouteT t r m a = SetRouteT { unSetRouteT :: EventWriterT t (Endo r) m a }
234− deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, MonadIO, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, MonadReflexCreateTrigger t, HasDocument)
237+ deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, MonadIO, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, MonadReflexCreateTrigger t, HasDocument, DomRenderHook t)
235238
236239instance (MonadFix m, MonadHold t m, DomBuilder t m) => DomBuilder t (SetRouteT t r m) where
237240 type DomBuilderSpace (SetRouteT t r m) = DomBuilderSpace m
@@ -261,8 +264,14 @@ instance (Reflex t, Monad m) => SetRoute t r (SetRouteT t r m) where
261264instance (Monad m, SetRoute t r m) => SetRoute t r (RoutedT t r' m) where
262265 modifyRoute = lift . modifyRoute
263266
264−instance Prerender js m => Prerender js (SetRouteT t r m) where
265− prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
267+instance (PerformEvent t m, Prerender js t m, Monad m, Reflex t) => Prerender js t (SetRouteT t r m) where
268+ type Client (SetRouteT t r m) = SetRouteT t r (Client m)
269+ prerender server client = do
270+ d <- lift $ prerender (runSetRouteT server) (runSetRouteT client)
271+ let (a, r) = splitDynPure d
272+ -- Must be prompt here
273+ SetRouteT . tellEvent $ switchPromptlyDyn r
274+ pure a
266275
267276instance Requester t m => Requester t (SetRouteT t r m) where
268277 type Request (SetRouteT t r m) = Request m
@@ -308,7 +317,7 @@ class RouteToUrl r m | m -> r where
308317 askRouteToUrl :: m (r -> Text)
309318
310319newtype RouteToUrlT r m a = RouteToUrlT { unRouteToUrlT :: ReaderT (r -> Text) m a }
311− deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, MonadIO, MonadReflexCreateTrigger t, HasDocument)
320+ deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, MonadIO, MonadReflexCreateTrigger t, HasDocument, DomRenderHook t)
312321
313322runRouteToUrlT
314323 :: RouteToUrlT r m a
@@ -332,8 +341,11 @@ instance HasJSContext m => HasJSContext (RouteToUrlT r m) where
332341 type JSContextPhantom (RouteToUrlT r m) = JSContextPhantom m
333342 askJSContext = lift askJSContext
334343
335−instance Prerender js m => Prerender js (RouteToUrlT r m) where
336− prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
344+instance (Prerender js t m, Monad m) => Prerender js t (RouteToUrlT r m) where
345+ type Client (RouteToUrlT r m) = RouteToUrlT r (Client m)
346+ prerender server client = do
347+ r <- RouteToUrlT ask
348+ lift $ prerender (runRouteToUrlT server r) (runRouteToUrlT client r)
337349
338350instance Requester t m => Requester t (RouteToUrlT r m) where
339351 type Request (RouteToUrlT r m) = Request m
@@ -393,10 +405,11 @@ runRouteViewT
393405 , MonadFix m
394406 )
395407 => (Encoder Identity Identity r PageName)
408+ -> Event t () -- ^ Switchover event, nothing is done until this event fires. Used to prevent incorrect DOM expectations at hydration switchover time
396409 -> RoutedT t r (SetRouteT t r (RouteToUrlT r m)) a
397410 -> m a
398−runRouteViewT routeEncoder a = do
399− rec historyState <- manageHistory $ HistoryCommand_PushState <$> setState
411+runRouteViewT routeEncoder switchover a = do
412+ rec historyState <- manageHistory' switchover $ HistoryCommand_PushState <$> setState
400413 let theEncoder = pageNameEncoder . hoistParse (pure . runIdentity) routeEncoder
401414 -- NB: The can only fail if the uriPath doesn't begin with a '/' or if the uriQuery
402415 -- is nonempty, but begins with a character that isn't '?'. Since we don't expect
lib/run/obelisk-run.cabal−1
@@ -25,7 +25,6 @@ library
2525 , obelisk-asset-serve-snap
2626 , obelisk-backend
2727 , obelisk-executable-config
28− , obelisk-executable-config-inject
2928 , obelisk-frontend
3029 , obelisk-route
3130 , process
lib/run/src/Obelisk/Run.hs+1−2
@@ -50,7 +50,6 @@ import Network.WebSockets (ConnectionOptions)
5050import Network.WebSockets.Connection (defaultConnectionOptions)
5151import qualified Obelisk.Asset.Serve.Snap as Snap
5252import Obelisk.ExecutableConfig (get)
53−import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
5453import Obelisk.Frontend
5554import Obelisk.Route.Frontend
5655import Reflex.Dom.Core
@@ -152,7 +151,7 @@ renderJsaddleFrontend :: (route -> Text) -> route -> Frontend route -> IO ByteSt
152151renderJsaddleFrontend urlEnc r f =
153152 let jsaddleScript = elAttr "script" ("src" =: "/jsaddle/jsaddle.js") blank
154153 jsaddlePreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "/jsaddle/jsaddle.js") blank
155− in renderFrontendHtml urlEnc r (_frontend_head f >> injectExecutableConfigs >> jsaddlePreload) (_frontend_body f >> jsaddleScript)
154+ in renderFrontendHtml urlEnc r (_frontend_head f >> jsaddlePreload) (_frontend_body f >> jsaddleScript)
156155
157156-- | like 'bindPortTCP' but reconnects on exception
158157bindPortTCPRetry :: Settings
skeleton/common/common.cabal+3−1
@@ -9,7 +9,9 @@ library
99 , obelisk-route
1010 , mtl
1111 , text
12−
12+ default-extensions:
13+ TypeFamilies
14+ PolyKinds
1315 exposed-modules:
1416 Common.Api
1517 Common.Route