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