@@ -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
@@ -17,66 +19,98 @@ module Language.Javascript.JSaddle.WebSockets (
18 , jsaddleApp
20 , jsaddleApp
19 , jsaddleWithAppOr
21 , jsaddleWithAppOr
20 , jsaddleAppPartial
24 , jsaddleAppPartial
23−import Control.Monad (forever)
32+import Control.Monad (when, void, forever)
24−import Control.Concurrent (forkIO, threadDelay)
33+import Control.Concurrent (killThread, forkIO, threadDelay)
25−import Control.Exception (handle, AsyncException, throwIO, fromException)
34+import Control.Exception (handle, AsyncException, throwIO, fromException, finally)
27import Data.Monoid ((<>))
36import Data.Monoid ((<>))
28import Data.Aeson (encode, decode)
37import Data.Aeson (encode, decode)
30import Network.Wai
39import Network.Wai
31− (requestBody, Application, Request, Response, ResponseReceived)
40+ (Middleware, lazyRequestBody, Application, Request, Response,
32import Network.WebSockets
42import Network.WebSockets
33− (ConnectionOptions(..), sendTextData, receiveDataMessage,
43+ (defaultConnectionOptions, ConnectionOptions(..), sendTextData,
34− acceptRequest, ServerApp, sendPing)
44+ receiveDataMessage, acceptRequest, ServerApp, sendPing)
35−import qualified Network.WebSockets as WS (DataMessage(..))
36import Network.Wai.Handler.WebSockets (websocketsOr)
45import Network.Wai.Handler.WebSockets (websocketsOr)
46+import Network.HTTP.Types (Status(..))
38−import Language.Javascript.JSaddle.Types (JSM(..))
48+import Language.Javascript.JSaddle.Types (JSM(..), JSContextRef(..))
39import qualified Network.Wai as W
49import qualified Network.Wai as W
40− (responseLBS, requestMethod, pathInfo)
50+ (responseLBS, requestMethod, pathInfo, modifyResponse, responseStatus)
41import qualified Data.Text as T (pack)
51import qualified Data.Text as T (pack)
42import qualified Network.HTTP.Types as H
52import qualified Network.HTTP.Types as H
43 (status403, status200)
53 (status403, status200)
44−import Language.Javascript.JSaddle.Run (runJavaScript)
54+import Language.Javascript.JSaddle.Run (syncPoint, runJavaScript)
45import Language.Javascript.JSaddle.Run.Files (indexHtml, runBatch, ghcjsHelpers, initState)
55import Language.Javascript.JSaddle.Run.Files (indexHtml, runBatch, ghcjsHelpers, initState)
56+import Language.Javascript.JSaddle.Debug
57+ (removeContext, addContext)
46import Data.Maybe (fromMaybe)
58import Data.Maybe (fromMaybe)
47import qualified Data.Map as M (empty, insert, lookup)
59import qualified Data.Map as M (empty, insert, lookup)
48−import Data.IORef (readIORef, newIORef, atomicModifyIORef')
61+ (readIORef, newIORef, atomicModifyIORef')
49−import Data.ByteString.Lazy (fromStrict, ByteString)
62+import Data.ByteString.Lazy (ByteString)
50−import Data.Text (Text)
63+import qualified Data.ByteString.Lazy as LBS
51−import Data.UUID.Types (toText)
64+ (toStrict, stripPrefix)
52−import Data.UUID.V4 (nextRandom)
65+import Control.Concurrent.MVar
66+ (tryTakeMVar, MVar, tryPutMVar, modifyMVar_, putMVar, takeMVar,
67+ readMVar, newMVar, newEmptyMVar, modifyMVar)
68+import Network.Wai.Handler.Warp
69+ (defaultSettings, setTimeout, setPort, runSettings)
70+import Foreign.Store (readStore, lookupStore, writeStore, Store(..))
71+import Language.Javascript.JSaddle (askJSM)
72+import Control.Monad.IO.Class (MonadIO(..))
74+import Language.Javascript.JSaddle.WebSockets.Compat (getTextMessageByteString)
75+import qualified Data.Text.Encoding as T (decodeUtf8)
54jsaddleOr :: ConnectionOptions -> JSM () -> Application -> IO Application
77jsaddleOr :: ConnectionOptions -> JSM () -> Application -> IO Application
55jsaddleOr opts entryPoint otherApp = do
78jsaddleOr opts entryPoint otherApp = do
56 syncHandlers <- newIORef M.empty
79 syncHandlers <- newIORef M.empty
80+ asyncHandlers <- newIORef M.empty
57 let wsApp :: ServerApp
81 let wsApp :: ServerApp
58 wsApp pending_conn = do
82 wsApp pending_conn = do
59 conn <- acceptRequest pending_conn
83 conn <- acceptRequest pending_conn
84+ initMsg <- receiveDataMessage conn
85+ case getTextMessageByteString initMsg of
60− (processResult, processSyncResult, start) <- runJavaScript (sendTextData conn . encode) entryPoint
87+ rec (processResult, processSyncResult, start) <- runJavaScript (sendTextData conn . encode) $ do
61− _ <- forkIO . forever $
88+ syncKey <- T.pack . show . contextId <$> askJSM
62− receiveDataMessage conn >>= \case
89+ liftIO $ atomicModifyIORef' syncHandlers (\m -> (M.insert syncKey processSyncResult m, ()))
63− (WS.Text t) ->
90+ liftIO $ atomicModifyIORef' asyncHandlers (\m -> (M.insert syncKey processResult m, ()))
91+ liftIO $ sendTextData conn (encode syncKey)
96+ M.lookup (T.decodeUtf8 $ LBS.toStrict syncKey) <$> readIORef syncHandlers >>= \case
97+ Nothing -> error "jsaddle missing sync message handler"
98+ Just processResult ->
100+ receiveDataMessage conn >>= \msg -> case getTextMessageByteString msg of
64− case decode t of
102+ case decode t of
65− Nothing -> error $ "jsaddle Results decode failed : " <> show t
103+ Nothing -> error $ "jsaddle Results decode failed : " <> show t
66− Just r -> processResult r
104+ Just r -> processResult r
67− _ -> error "jsaddle WebSocket unexpected binary data"
105+ _ -> error "jsaddle WebSocket unexpected binary data"
68− syncKey <- toText <$> nextRandom
106+ _ -> error "jsaddle WebSocket unexpected binary data"
69− atomicModifyIORef' syncHandlers (\m -> (M.insert syncKey processSyncResult m, ()))
70− sendTextData conn (encode syncKey)
74 -- Based on Network.WebSocket.forkPingThread
108 -- Based on Network.WebSocket.forkPingThread
75 waitTillClosed conn = ignore `handle` go 1
109 waitTillClosed conn = ignore `handle` go 1
77 go :: Int -> IO ()
111 go :: Int -> IO ()
78 go i = do
112 go i = do
79− threadDelay (1 * 1000 * 1000)
113+ threadDelay (10 * 1000 * 1000)
80 sendPing conn (T.pack $ show i)
114 sendPing conn (T.pack $ show i)
81 go (i + 1)
115 go (i + 1)
@@ -87,8 +121,8 @@ jsaddleOr opts entryPoint otherApp = do
87 syncHandler :: Application
121 syncHandler :: Application
88 syncHandler req sendResponse = case (W.requestMethod req, W.pathInfo req) of
122 syncHandler req sendResponse = case (W.requestMethod req, W.pathInfo req) of
89 ("POST", ["sync", syncKey]) -> do
123 ("POST", ["sync", syncKey]) -> do
90− body <- requestBody req
124+ body <- lazyRequestBody req
91− case decode $ fromStrict body of
125+ case decode body of
92 Nothing -> error $ "jsaddle sync message decode failed : " <> show body
126 Nothing -> error $ "jsaddle sync message decode failed : " <> show body
93 Just result ->
127 Just result ->
94 M.lookup syncKey <$> readIORef syncHandlers >>= \case
128 M.lookup syncKey <$> readIORef syncHandlers >>= \case
@@ -96,15 +130,27 @@ jsaddleOr opts entryPoint otherApp = do
96 Just handler -> do
130 Just handler -> do
97 next <- encode <$> handler result
131 next <- encode <$> handler result
98 sendResponse $ W.responseLBS H.status200 [("Content-Type", "application/json")] next
132 sendResponse $ W.responseLBS H.status200 [("Content-Type", "application/json")] next
99− _ -> otherApp req sendResponse
133+ (method, _) -> (catch404 otherApp) req sendResponse
134+ where catch404 = W.modifyResponse $ \resp ->
135+ case (method, W.responseStatus resp) of
136+ ("GET", Status 404 _) -> indexResponse
100 return $ websocketsOr opts wsApp syncHandler
138 return $ websocketsOr opts wsApp syncHandler
103jsaddleApp :: Application
141jsaddleApp :: Application
142+jsaddleApp = jsaddleAppWithJs $ jsaddleJs False
144+jsaddleAppWithJs :: ByteString -> Application
104−jsaddleApp req sendResponse =
145+jsaddleAppWithJs js req sendResponse =
105− fromMaybe
146+ jsaddleAppWithJsOr js
106− (sendResponse $ W.responseLBS H.status403 [("Content-Type", "text/plain")] "Forbidden")
147+ (\_ _ -> sendResponse $ W.responseLBS H.status403 [("Content-Type", "text/plain")] "Forbidden")
150+jsaddleAppWithJsOr :: ByteString -> Application -> Application
151+jsaddleAppWithJsOr js otherApp req sendResponse =
152+ fromMaybe (otherApp req sendResponse)
107− (jsaddleAppPartial req sendResponse)
153+ (jsaddleAppPartialWithJs js req sendResponse)
109jsaddleWithAppOr :: ConnectionOptions -> JSM () -> Application -> IO Application
155jsaddleWithAppOr :: ConnectionOptions -> JSM () -> Application -> IO Application
110jsaddleWithAppOr opts entryPoint otherApp = jsaddleOr opts entryPoint $ \req sendResponse ->
156jsaddleWithAppOr opts entryPoint otherApp = jsaddleOr opts entryPoint $ \req sendResponse ->
@@ -112,46 +158,82 @@ jsaddleWithAppOr opts entryPoint otherApp = jsaddleOr opts entryPoint $ \req sen
112 (jsaddleAppPartial req sendResponse))
158 (jsaddleAppPartial req sendResponse))
114jsaddleAppPartial :: Request -> (Response -> IO ResponseReceived) -> Maybe (IO ResponseReceived)
160jsaddleAppPartial :: Request -> (Response -> IO ResponseReceived) -> Maybe (IO ResponseReceived)
161+jsaddleAppPartial = jsaddleAppPartialWithJs $ jsaddleJs False
163+indexResponse :: Response
164+indexResponse = W.responseLBS H.status200 [("Content-Type", "text/html")] indexHtml
166+jsaddleAppPartialWithJs :: ByteString -> Request -> (Response -> IO ResponseReceived) -> Maybe (IO ResponseReceived)
115−jsaddleAppPartial req sendResponse = case (W.requestMethod req, W.pathInfo req) of
167+jsaddleAppPartialWithJs js req sendResponse = case (W.requestMethod req, W.pathInfo req) of
116− ("GET", []) -> Just $ sendResponse $ W.responseLBS H.status200 [("Content-Type", "text/html")] indexHtml
168+ ("GET", []) -> Just $ sendResponse indexResponse
117− ("GET", ["jsaddle.js"]) -> Just $ sendResponse $ W.responseLBS H.status200 [("Content-Type", "application/javascript")] jsaddleJs
169+ ("GET", ["jsaddle.js"]) -> Just $ sendResponse $ W.responseLBS H.status200 [("Content-Type", "application/javascript")] js
118 _ -> Nothing
170 _ -> Nothing
172+jsaddleJs :: Bool -> ByteString
173+jsaddleJs = jsaddleJs' Nothing
120-- Use this to generate this string for embedding
175-- Use this to generate this string for embedding
121-- sed -e 's|\\|\\\\|g' -e 's|^| \\|' -e 's|$|\\n\\|' -e 's|"|\\"|g' data/jsaddle.js | pbcopy
176-- sed -e 's|\\|\\\\|g' -e 's|^| \\|' -e 's|$|\\n\\|' -e 's|"|\\"|g' data/jsaddle.js | pbcopy
122−jsaddleJs :: ByteString
177+jsaddleJs' :: Maybe ByteString -> Bool -> ByteString
123−jsaddleJs = "\
178+jsaddleJs' jsaddleUri refreshOnLoad = "\
124− \if(typeof global !== \"undefined\") {\n\
179+ \if(typeof global !== \"undefined\" && typeof require === \"function\") {\n\
125 \ global.window = global;\n\
180 \ global.window = global;\n\
126 \ global.WebSocket = require('ws');\n\
181 \ global.WebSocket = require('ws');\n\
129 \var connect = function() {\n\
184 \var connect = function() {\n\
130− \ var wsaddress = window.location.protocol.replace('http', 'ws')+\"//\"+window.location.hostname+(window.location.port?(\":\"+window.location.port):\"\");\n\
186+ <> maybe "window.location.protocol.replace('http', 'ws')+\"//\"+window.location.hostname+(window.location.port?(\":\"+window.location.port):\"\")"
187+ (\ s -> "\"ws" <> s <> "\"")
188+ (jsaddleUri >>= LBS.stripPrefix "http")
132− \ var ws = new WebSocket(wsaddress);\n\
191+ \ var ws0 = new WebSocket(wsaddress);\n\
133 \ var syncKey = \"\";\n\
192 \ var syncKey = \"\";\n\
135− \ ws.onopen = function(e) {\n\
194+ \ ws0.onopen = function(e) {\n\
195+ \ ws0.send(\"\");\n\
196+ \ var initialResults = [];\n\
197+ \ var ws = {send: function(m) {initialResults.push(m);}};\n\
136− \ " <> initState <> "\
198+ \ " <> initState <> "\n\
138− \ ws.onmessage = function(e) {\n\
200+ \ ws0.onmessage = function(e) {\n\
139 \ var batch = JSON.parse(e.data);\n\
201 \ var batch = JSON.parse(e.data);\n\
202+ \ if(inCallback > 0) {\n\
203+ \ asyncBatch = batch;\n\
140 \ if(typeof batch === \"string\") {\n\
206 \ if(typeof batch === \"string\") {\n\
141 \ syncKey = batch;\n\
207 \ syncKey = batch;\n\
208+ \ var ws1 = new WebSocket(wsaddress);\n\
209+ \ ws1.onopen = function(e) {\n\
210+ \ ws1.send(syncKey);\n\
211+ \ initialResults.forEach(function(m){ ws1.send(m); });\n\
212+ \ initialResults = null;\n\
216+ then " var xhr = new XMLHttpRequest();\n\
217+ \ xhr.open('POST', '/reload/'+syncKey, true);\n\
218+ \ xhr.onreadystatechange = function() {\n\
219+ \ if(xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200)\n\
220+ \ setTimeout(function(){window.location.reload();}, 100);\n\
142− \ return;\n\
224+ " return;\n\
145 \ " <> runBatch (\a -> "ws.send(JSON.stringify(" <> a <> "));")
227 \ " <> runBatch (\a -> "ws.send(JSON.stringify(" <> a <> "));")
146 (Just (\a -> "(function(){\n\
228 (Just (\a -> "(function(){\n\
147 \ var xhr = new XMLHttpRequest();\n\
229 \ var xhr = new XMLHttpRequest();\n\
148− \ xhr.open('POST', '/sync/'+syncKey, false);\n\
230+ \ xhr.open('POST', '" <> fromMaybe "" jsaddleUri <> "/sync/'+syncKey, false);\n\
149 \ xhr.setRequestHeader(\"Content-type\", \"application/json\");\n\
231 \ xhr.setRequestHeader(\"Content-type\", \"application/json\");\n\
150 \ xhr.send(JSON.stringify(" <> a <> "));\n\
232 \ xhr.send(JSON.stringify(" <> a <> "));\n\
151 \ return JSON.parse(xhr.response);})()")) <> "\
233 \ return JSON.parse(xhr.response);})()")) <> "\
154− \ ws.onerror = function() {\n\
236+ \ ws0.onerror = function() {\n\
155 \ setTimeout(connect, 1000);\n\
237 \ setTimeout(connect, 1000);\n\
@@ -160,3 +242,78 @@ jsaddleJs = "\
160 \connect();\n\
242 \connect();\n\
245+-- | Start or restart the server.
246+-- To run this as part of every :reload use
247+-- > :def! reload (const $ return "::reload\nLanguage.Javascript.JSaddle.Warp.debug 3708 SomeMainModule.someMainFunction")
248+debug :: Int -> JSM () -> IO ()
250+ debugWrapper $ \withRefresh registerContext ->
251+ runSettings (setPort port (setTimeout 3600 defaultSettings)) =<<
252+ jsaddleOr defaultConnectionOptions (registerContext >> f >> syncPoint) (withRefresh $ jsaddleAppWithJs $ jsaddleJs True)
253+ putStrLn $ "<a href=\"http://localhost:" <> show port <> "\">run</a>"
255+debugOr :: Int -> JSM () -> Application -> IO ()
256+debugOr port f b = do
257+ debugWrapper $ \withRefresh registerContext ->
258+ runSettings (setPort port (setTimeout 3600 defaultSettings)) =<<
259+ jsaddleOr defaultConnectionOptions (registerContext >> f >> syncPoint) (withRefresh $ jsaddleAppWithJsOr (jsaddleJs True) b)
260+ putStrLn $ "<a href=\"http://localhost:" <> show port <> "\">run</a>"
262+refreshMiddleware :: ((Response -> IO ResponseReceived) -> IO ResponseReceived) -> Middleware
263+refreshMiddleware refresh otherApp req sendResponse = case (W.requestMethod req, W.pathInfo req) of
264+ ("POST", ["reload", _syncKey]) -> refresh sendResponse
265+ _ -> otherApp req sendResponse
267+debugWrapper :: (Middleware -> JSM () -> IO ()) -> IO ()
268+debugWrapper run = do
269+ reloadMVar <- newEmptyMVar
270+ reloadDoneMVars <- newMVar []
271+ contexts <- newMVar []
272+ let refresh sendResponse = do
273+ reloadDone <- newEmptyMVar
274+ modifyMVar_ reloadDoneMVars (return . (reloadDone:))
276+ r <- sendResponse $ W.responseLBS H.status200 [("Content-Type", "application/json")] ("reload" :: ByteString)
277+ putMVar reloadDone ()
279+ start :: Int -> IO (IO Int)
280+ start expectedConnections = do
281+ serverDone <- newEmptyMVar
282+ ready <- newEmptyMVar
283+ let registerContext :: JSM ()
284+ registerContext = do
285+ uuid <- contextId <$> askJSM
286+ browsersConnected <- liftIO $ modifyMVar contexts (\ctxs -> return (uuid:ctxs, length ctxs + 1))
288+ when (browsersConnected == expectedConnections) . void . liftIO $ tryPutMVar ready ()
290+ finally (run (refreshMiddleware refresh) registerContext)
291+ (putMVar serverDone ())
292+ _ <- forkIO $ threadDelay 10000000 >> void (tryPutMVar ready ())
293+ when (expectedConnections /= 0) $ takeMVar ready
295+ putMVar reloadMVar ()
296+ ctxs <- takeMVar contexts
297+ mapM_ removeContext ctxs
298+ takeMVar reloadDoneMVars >>= mapM_ takeMVar
299+ tryTakeMVar serverDone >>= \case
304+ return $ length ctxs
305+ restarter :: MVar (Int -> IO (IO Int)) -> IO Int -> IO ()
306+ restarter mvar stop = do
307+ start' <- takeMVar mvar
309+ start' n >>= restarter mvar
310+ lookupStore storeId >>= \case
312+ restartMVar <- newMVar start
313+ void . forkIO $ restarter restartMVar (return 0)
314+ void $ writeStore (Store storeId) restartMVar
315+ Just shutdownStore -> do
316+ restartMVar :: MVar (Int -> IO (IO Int)) <- readStore shutdownStore
317+ void $ tryTakeMVar restartMVar
318+ putMVar restartMVar start