Skip to content

Commit b0d0ccc

Browse files
author
Lucas V. R
committed
Server customization
Allows Ema users to specify the Ema Shim or a custom websocket response. This can be used, for instance, to make the server open files in the user's favorite editor in response to a websocket request.
1 parent 1827597 commit b0d0ccc

2 files changed

Lines changed: 56 additions & 13 deletions

File tree

ema/src/Ema/App.hs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module Ema.App (
44
runSite,
55
runSite_,
66
runSiteWithCli,
7+
runSiteWithServerOpts,
78
) where
89

910
import Control.Concurrent (threadDelay)
@@ -65,7 +66,21 @@ runSiteWithCli ::
6566
RouteModel r
6667
, DSum CLI.Action Identity
6768
)
68-
runSiteWithCli cli siteArg = do
69+
runSiteWithCli = runSiteWithServerOpts @r Server.defaultEmaServerOptions
70+
71+
-- | Like @runSiteWithCli@ but takes Ema server options.
72+
runSiteWithServerOpts ::
73+
forall r.
74+
(Show r, Eq r, EmaStaticSite r) =>
75+
Server.EmaServerOptions r ->
76+
CLI.Cli ->
77+
SiteArg r ->
78+
IO
79+
( -- The initial model value.
80+
RouteModel r
81+
, DSum CLI.Action Identity
82+
)
83+
runSiteWithServerOpts opts cli siteArg = do
6984
flip runLoggerLoggingT (getLogger cli) $ do
7085
cwd <- liftIO getCurrentDirectory
7186
logInfoNS "ema" $ "Launching Ema under: " <> toText cwd
@@ -88,6 +103,6 @@ runSiteWithCli cli siteArg = do
88103
liftIO $ threadDelay maxBound
89104
)
90105
( flip runLoggingT logger $ do
91-
Server.runServerWithWebSocketHotReload @r host mport model
106+
Server.runServerWithWebSocketHotReload @r opts host mport model
92107
)
93108
pure (model0, act :=> Identity ())

ema/src/Ema/Server.hs

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,35 @@ import UnliftIO.Async (race)
3838
import UnliftIO.Concurrent (threadDelay)
3939
import UnliftIO.Exception (catch, try)
4040

41+
{- | A handler takes a websocket connection and the current model and then watches
42+
for websocket messages. It must return a new route to watch (after that, the
43+
returned route's HTML will be sent back to the client).
44+
45+
Note that this is usually a long-running thread that waits for the client's
46+
messages. But you can also use it to implement custom server actions, by handling
47+
the incoming websocket messages or other IO events in any way you like.
48+
49+
Also note that whenever the model is updated, the handler action will be
50+
stopped and then restarted with the new model as argument.
51+
-}
52+
type EmaWsHandler r = WS.Connection -> RouteModel r -> LoggingT IO Text
53+
54+
data EmaServerOptions r = EmaServerOptions
55+
{ emaServerShim :: LByteString
56+
, emaServerWsHandler :: EmaWsHandler r
57+
}
58+
59+
defaultEmaWsHandler :: forall r. EmaWsHandler r
60+
defaultEmaWsHandler conn _model = do
61+
msg :: Text <- liftIO $ WS.receiveData conn
62+
log LevelDebug $ "<~~ " <> show msg
63+
pure msg
64+
where
65+
log lvl (t :: Text) = logWithoutLoc "ema.ws" lvl t
66+
67+
defaultEmaServerOptions :: forall r. EmaServerOptions r
68+
defaultEmaServerOptions = EmaServerOptions wsClientJS (defaultEmaWsHandler @r)
69+
4170
runServerWithWebSocketHotReload ::
4271
forall r m.
4372
( Show r
@@ -48,11 +77,12 @@ runServerWithWebSocketHotReload ::
4877
, IsRoute r
4978
, EmaStaticSite r
5079
) =>
80+
EmaServerOptions r ->
5181
Host ->
5282
Maybe Port ->
5383
LVar (RouteModel r) ->
5484
m ()
55-
runServerWithWebSocketHotReload host mport model = do
85+
runServerWithWebSocketHotReload opts host mport model = do
5686
logger <- askLoggerIO
5787
let runM = flip runLoggingT logger
5888
settings =
@@ -90,10 +120,7 @@ runServerWithWebSocketHotReload host mport model = do
90120
let log lvl (s :: Text) =
91121
logWithoutLoc (toText @String $ printf "ema.ws.%.2d" subId) lvl s
92122
log LevelInfo "Connected"
93-
let askClientForRoute = do
94-
msg :: Text <- liftIO $ WS.receiveData conn
95-
log LevelDebug $ "<~~ " <> show msg
96-
pure msg
123+
let wsHandler = emaServerWsHandler opts conn
97124
sendRouteHtmlToClient path s = do
98125
decodeUrlRoute s path & \case
99126
Left err -> do
@@ -113,10 +140,11 @@ runServerWithWebSocketHotReload host mport model = do
113140
liftIO $ WS.sendTextData conn $ "REDIRECT " <> toText (review (fromPrism_ $ enc s) r)
114141
log LevelDebug $ " ~~> " <> show r
115142
-- @mWatchingRoute@ is the route currently being watched.
116-
loop mWatchingRoute =
143+
loop mWatchingRoute = do
117144
-- Listen *until* either we get a new value, or the client requests
118145
-- to switch to a new route.
119-
race (LVar.listenNext model subId) askClientForRoute >>= \case
146+
currentModel <- LVar.get model
147+
race (LVar.listenNext model subId) (wsHandler currentModel) >>= \case
120148
Left newModel -> do
121149
-- The page the user is currently viewing has changed. Send
122150
-- the new HTML to them.
@@ -130,7 +158,7 @@ runServerWithWebSocketHotReload host mport model = do
130158
sendRouteHtmlToClient mNextRoute =<< LVar.get model
131159
loop mNextRoute
132160
-- Wait for the client to send the first request with the initial route.
133-
mInitialRoute <- askClientForRoute
161+
mInitialRoute <- wsHandler =<< LVar.get model
134162
try (loop mInitialRoute) >>= \case
135163
Right () -> pass
136164
Left (connExc :: ConnectionException) -> do
@@ -150,18 +178,18 @@ runServerWithWebSocketHotReload host mport model = do
150178
case mr of
151179
Left err -> do
152180
logErrorNS "App" $ badRouteEncodingMsg err
153-
let s = emaErrorHtmlResponse (badRouteEncodingMsg err) <> wsClientJS
181+
let s = emaErrorHtmlResponse (badRouteEncodingMsg err) <> emaServerShim opts
154182
liftIO $ f $ Wai.responseLBS H.status500 [(H.hContentType, "text/html")] s
155183
Right Nothing -> do
156-
let s = emaErrorHtmlResponse decodeRouteNothingMsg <> wsClientJS
184+
let s = emaErrorHtmlResponse decodeRouteNothingMsg <> emaServerShim opts
157185
liftIO $ f $ Wai.responseLBS H.status404 [(H.hContentType, "text/html")] s
158186
Right (Just r) -> do
159187
renderCatchingErrors val r >>= \case
160188
AssetStatic staticPath -> do
161189
let mimeType = Static.getMimeType staticPath
162190
liftIO $ f $ Wai.responseFile H.status200 [(H.hContentType, mimeType)] staticPath Nothing
163191
AssetGenerated Html html -> do
164-
let s = html <> toLazy wsClientHtml <> wsClientJS
192+
let s = html <> toLazy wsClientHtml <> emaServerShim opts
165193
liftIO $ f $ Wai.responseLBS H.status200 [(H.hContentType, "text/html")] s
166194
AssetGenerated Other s -> do
167195
let mimeType = Static.getMimeType $ review (fromPrism_ $ enc val) r

0 commit comments

Comments
 (0)