@@ -1,5 +1,7 @@
1{-# LANGUAGE LambdaCase #-}
1{-# LANGUAGE LambdaCase #-}
2{-# LANGUAGE OverloadedStrings #-}
2{-# LANGUAGE OverloadedStrings #-}
3+{-# LANGUAGE ScopedTypeVariables #-}
4+{-# LANGUAGE RecursiveDo #-}
3-----------------------------------------------------------------------------
5-----------------------------------------------------------------------------
5-- Module : Language.Javascript.JSaddle.WebSockets
7-- Module : Language.Javascript.JSaddle.WebSockets
@@ -18,38 +20,50 @@ module Language.Javascript.JSaddle.WebSockets (
18 , jsaddleApp
20 , jsaddleApp
19 , jsaddleWithAppOr
21 , jsaddleWithAppOr
20 , jsaddleAppPartial
22 , jsaddleAppPartial
23−import Control.Monad (forever)
27+import Control.Monad (when, join, void, forever)
24−import Control.Concurrent (forkIO, threadDelay)
28+import Control.Concurrent (killThread, forkIO, threadDelay)
25−import Control.Exception (handle, AsyncException, throwIO, fromException)
29+import Control.Exception (handle, AsyncException, throwIO, fromException, finally)
27import Data.Monoid ((<>))
31import Data.Monoid ((<>))
28import Data.Aeson (encode, decode)
32import Data.Aeson (encode, decode)
30import Network.Wai
34import Network.Wai
31− (lazyRequestBody, Application, Request, Response, ResponseReceived)
35+ (Middleware, lazyRequestBody, Application, Request, Response,
32import Network.WebSockets
37import Network.WebSockets
33− (ConnectionOptions(..), sendTextData, receiveDataMessage,
38+ (defaultConnectionOptions, ConnectionOptions(..), sendTextData,
34− acceptRequest, ServerApp, sendPing)
39+ receiveDataMessage, acceptRequest, ServerApp, sendPing)
35import qualified Network.WebSockets as WS (DataMessage(..))
40import qualified Network.WebSockets as WS (DataMessage(..))
36import Network.Wai.Handler.WebSockets (websocketsOr)
41import Network.Wai.Handler.WebSockets (websocketsOr)
38−import Language.Javascript.JSaddle.Types (JSM(..))
43+import Language.Javascript.JSaddle.Types (JSM(..), JSContextRef(..))
39import qualified Network.Wai as W
44import qualified Network.Wai as W
40 (responseLBS, requestMethod, pathInfo)
45 (responseLBS, requestMethod, pathInfo)
41import qualified Data.Text as T (pack)
46import qualified Data.Text as T (pack)
42import qualified Network.HTTP.Types as H
47import qualified Network.HTTP.Types as H
43 (status403, status200)
48 (status403, status200)
44−import Language.Javascript.JSaddle.Run (runJavaScript)
49+import Language.Javascript.JSaddle.Run (syncPoint, runJavaScript)
45import Language.Javascript.JSaddle.Run.Files (indexHtml, runBatch, ghcjsHelpers, initState)
50import Language.Javascript.JSaddle.Run.Files (indexHtml, runBatch, ghcjsHelpers, initState)
51+import Language.Javascript.JSaddle.Debug
52+ (removeContext, addContext)
46import Data.Maybe (fromMaybe)
53import Data.Maybe (fromMaybe)
47import qualified Data.Map as M (empty, insert, lookup)
54import qualified Data.Map as M (empty, insert, lookup)
48−import Data.IORef (readIORef, newIORef, atomicModifyIORef')
56+ (writeIORef, IORef, readIORef, newIORef, atomicModifyIORef')
49−import Data.ByteString.Lazy (fromStrict, ByteString)
57+import Data.ByteString.Lazy (ByteString)
50−import Data.Text (Text)
51import Data.UUID.Types (toText)
58import 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(..))
54jsaddleOr :: ConnectionOptions -> JSM () -> Application -> IO Application
68jsaddleOr :: ConnectionOptions -> JSM () -> Application -> IO Application
55jsaddleOr opts entryPoint otherApp = do
69jsaddleOr opts entryPoint otherApp = do
@@ -57,7 +71,11 @@ jsaddleOr opts entryPoint otherApp = do
57 let wsApp :: ServerApp
71 let wsApp :: ServerApp
58 wsApp pending_conn = do
72 wsApp pending_conn = do
59 conn <- acceptRequest pending_conn
73 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)
61 _ <- forkIO . forever $
79 _ <- forkIO . forever $
62 receiveDataMessage conn >>= \case
80 receiveDataMessage conn >>= \case
63 (WS.Text t) ->
81 (WS.Text t) ->
@@ -65,9 +83,6 @@ jsaddleOr opts entryPoint otherApp = do
65 Nothing -> error $ "jsaddle Results decode failed : " <> show t
83 Nothing -> error $ "jsaddle Results decode failed : " <> show t
66 Just r -> processResult r
84 Just r -> processResult r
67 _ -> error "jsaddle WebSocket unexpected binary data"
85 _ -> error "jsaddle WebSocket unexpected binary data"
68− syncKey <- toText <$> nextRandom
69− atomicModifyIORef' syncHandlers (\m -> (M.insert syncKey processSyncResult m, ()))
70− sendTextData conn (encode syncKey)
72 waitTillClosed conn
87 waitTillClosed conn
@@ -141,6 +156,13 @@ jsaddleJs = "\
141 \ var batch = JSON.parse(e.data);\n\
156 \ var batch = JSON.parse(e.data);\n\
142 \ if(typeof batch === \"string\") {\n\
157 \ if(typeof batch === \"string\") {\n\
143 \ syncKey = batch;\n\
158 \ 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\
144 \ return;\n\
166 \ return;\n\
@@ -164,3 +186,60 @@ jsaddleJs = "\
164 \connect();\n\
186 \connect();\n\
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 ()
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>"
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:))
210+ r <- sendResponse $ W.responseLBS H.status200 [("Content-Type", "application/json")] ("reload" :: ByteString)
211+ putMVar reloadDone ()
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))
223+ when (browsersConnected == expectedConnections) . void . liftIO $ tryPutMVar ready ()
225+ finally (run refreshMiddleware registerContext)
226+ (putMVar serverDone ())
227+ forkIO $ threadDelay 10000000 >> void (tryPutMVar ready ())
228+ when (expectedConnections /= 0) $ takeMVar ready
230+ putMVar reloadMVar ()
231+ ctxs <- takeMVar contexts
232+ mapM_ removeContext ctxs
233+ takeMVar reloadDoneMVars >>= mapM_ takeMVar
236+ return $ length ctxs
237+ lookupStore shutdown_0 >>= \case
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