@@ -1,5 +1,7 @@
11{-# LANGUAGE LambdaCase #-}
22{-# LANGUAGE OverloadedStrings #-}
3+{-# LANGUAGE ScopedTypeVariables #-}
4+{-# LANGUAGE RecursiveDo #-}
35-----------------------------------------------------------------------------
46--
57-- Module : Language.Javascript.JSaddle.WebSockets
@@ -18,38 +20,50 @@ module Language.Javascript.JSaddle.WebSockets (
1820 , jsaddleApp
1921 , jsaddleWithAppOr
2022 , jsaddleAppPartial
23+ , debug
24+ , debugWrapper
2125) where
2226
23−import Control.Monad (forever)
24−import Control.Concurrent (forkIO, threadDelay)
25−import Control.Exception (handle, AsyncException, throwIO, fromException)
27+import Control.Monad (when, join, void, forever)
28+import Control.Concurrent (killThread, forkIO, threadDelay)
29+import Control.Exception (handle, AsyncException, throwIO, fromException, finally)
2630
2731import Data.Monoid ((<>))
2832import Data.Aeson (encode, decode)
2933
3034import Network.Wai
31− (lazyRequestBody, Application, Request, Response, ResponseReceived)
35+ (Middleware, lazyRequestBody, Application, Request, Response,
36+ ResponseReceived)
3237import Network.WebSockets
33− (ConnectionOptions(..), sendTextData, receiveDataMessage,
34− acceptRequest, ServerApp, sendPing)
38+ (defaultConnectionOptions, ConnectionOptions(..), sendTextData,
39+ receiveDataMessage, acceptRequest, ServerApp, sendPing)
3540import qualified Network.WebSockets as WS (DataMessage(..))
3641import Network.Wai.Handler.WebSockets (websocketsOr)
3742
38−import Language.Javascript.JSaddle.Types (JSM(..))
43+import Language.Javascript.JSaddle.Types (JSM(..), JSContextRef(..))
3944import qualified Network.Wai as W
4045 (responseLBS, requestMethod, pathInfo)
4146import qualified Data.Text as T (pack)
4247import qualified Network.HTTP.Types as H
4348 (status403, status200)
44−import Language.Javascript.JSaddle.Run (runJavaScript)
49+import Language.Javascript.JSaddle.Run (syncPoint, runJavaScript)
4550import Language.Javascript.JSaddle.Run.Files (indexHtml, runBatch, ghcjsHelpers, initState)
51+import Language.Javascript.JSaddle.Debug
52+ (removeContext, addContext)
4653import Data.Maybe (fromMaybe)
4754import qualified Data.Map as M (empty, insert, lookup)
48−import Data.IORef (readIORef, newIORef, atomicModifyIORef')
49−import Data.ByteString.Lazy (fromStrict, ByteString)
50−import Data.Text (Text)
55+import Data.IORef
56+ (writeIORef, IORef, readIORef, newIORef, atomicModifyIORef')
57+import Data.ByteString.Lazy (ByteString)
5158import Data.UUID.Types (toText)
52−import Data.UUID.V4 (nextRandom)
59+import Control.Concurrent.MVar
60+ (tryPutMVar, modifyMVar_, putMVar, takeMVar, readMVar, newMVar,
61+ newEmptyMVar, modifyMVar)
62+import Network.Wai.Handler.Warp
63+ (defaultSettings, setTimeout, setPort, runSettings)
64+import Foreign.Store (newStore, readStore, lookupStore)
65+import Language.Javascript.JSaddle (askJSM)
66+import Control.Monad.IO.Class (MonadIO(..))
5367
5468jsaddleOr :: ConnectionOptions -> JSM () -> Application -> IO Application
5569jsaddleOr opts entryPoint otherApp = do
@@ -57,7 +71,11 @@ jsaddleOr opts entryPoint otherApp = do
5771 let wsApp :: ServerApp
5872 wsApp pending_conn = do
5973 conn <- acceptRequest pending_conn
60− (processResult, processSyncResult, start) <- runJavaScript (sendTextData conn . encode) entryPoint
74+ rec (processResult, processSyncResult, start) <- runJavaScript (sendTextData conn . encode) $ do
75+ syncKey <- toText . contextId <$> askJSM
76+ liftIO $ atomicModifyIORef' syncHandlers (\m -> (M.insert syncKey processSyncResult m, ()))
77+ liftIO $ sendTextData conn (encode syncKey)
78+ entryPoint
6179 _ <- forkIO . forever $
6280 receiveDataMessage conn >>= \case
6381 (WS.Text t) ->
@@ -65,9 +83,6 @@ jsaddleOr opts entryPoint otherApp = do
6583 Nothing -> error $ "jsaddle Results decode failed : " <> show t
6684 Just r -> processResult r
6785 _ -> error "jsaddle WebSocket unexpected binary data"
68− syncKey <- toText <$> nextRandom
69− atomicModifyIORef' syncHandlers (\m -> (M.insert syncKey processSyncResult m, ()))
70− sendTextData conn (encode syncKey)
7186 start
7287 waitTillClosed conn
7388
@@ -141,6 +156,13 @@ jsaddleJs = "\
141156 \ var batch = JSON.parse(e.data);\n\
142157 \ if(typeof batch === \"string\") {\n\
143158 \ syncKey = batch;\n\
159+ \ var xhr = new XMLHttpRequest();\n\
160+ \ xhr.open('POST', '/reload/'+syncKey, true);\n\
161+ \ xhr.onreadystatechange = function() {\n\
162+ \ if(xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200)\n\
163+ \ setTimeout(function(){window.location.reload();}, 500);\n\
164+ \ };\n\
165+ \ xhr.send();\n\
144166 \ return;\n\
145167 \ }\n\
146168 \\n\
@@ -164,3 +186,60 @@ jsaddleJs = "\
164186 \connect();\n\
165187 \"
166188
189+-- | Start or restart the server.
190+-- To run this as part of every :reload use
191+-- > :def! reload (const $ return "::reload\nLanguage.Javascript.JSaddle.Warp.debug 3708 SomeMainModule.someMainFunction")
192+debug :: Int -> JSM () -> IO ()
193+debug port f = do
194+ debugWrapper $ \refreshMiddleware registerContext ->
195+ runSettings (setPort port (setTimeout 3600 defaultSettings)) =<<
196+ jsaddleOr defaultConnectionOptions (registerContext >> f >> syncPoint) (refreshMiddleware jsaddleApp)
197+ putStrLn $ "<a href=\"http://localhost:" <> show port <> "\">run</a>"
198+
199+debugWrapper :: (Middleware -> JSM () -> IO ()) -> IO ()
200+debugWrapper run = do
201+ reloadMVar <- newEmptyMVar
202+ reloadDoneMVars <- newMVar []
203+ contexts <- newMVar []
204+ let refreshMiddleware :: Middleware
205+ refreshMiddleware otherApp req sendResponse = case (W.requestMethod req, W.pathInfo req) of
206+ ("POST", ["reload", _syncKey]) -> do
207+ reloadDone <- newEmptyMVar
208+ modifyMVar_ reloadDoneMVars (return . (reloadDone:))
209+ readMVar reloadMVar
210+ r <- sendResponse $ W.responseLBS H.status200 [("Content-Type", "application/json")] ("reload" :: ByteString)
211+ putMVar reloadDone ()
212+ return r
213+ _ -> otherApp req sendResponse
214+ start :: Int -> IO (IO Int)
215+ start expectedConnections = do
216+ serverDone <- newEmptyMVar
217+ ready <- newEmptyMVar
218+ let registerContext :: JSM ()
219+ registerContext = do
220+ uuid <- contextId <$> askJSM
221+ browsersConnected <- liftIO $ modifyMVar contexts (\ctxs -> return (uuid:ctxs, length ctxs + 1))
222+ addContext
223+ when (browsersConnected == expectedConnections) . void . liftIO $ tryPutMVar ready ()
224+ thread <- forkIO $
225+ finally (run refreshMiddleware registerContext)
226+ (putMVar serverDone ())
227+ forkIO $ threadDelay 10000000 >> void (tryPutMVar ready ())
228+ when (expectedConnections /= 0) $ takeMVar ready
229+ return $ do
230+ putMVar reloadMVar ()
231+ ctxs <- takeMVar contexts
232+ mapM_ removeContext ctxs
233+ takeMVar reloadDoneMVars >>= mapM_ takeMVar
234+ killThread thread
235+ takeMVar serverDone
236+ return $ length ctxs
237+ lookupStore shutdown_0 >>= \case
238+ Nothing -> do
239+ shutdownRef <- newIORef =<< start 0
240+ void $ newStore shutdownRef
241+ Just shutdownStore -> do
242+ shutdownRef :: IORef (IO Int) <- readStore shutdownStore
243+ expectedConnections <- join (readIORef shutdownRef)
244+ writeIORef shutdownRef =<< start expectedConnections
245+ where shutdown_0 = 0