@@ -19,17 +19,12 @@ module Language.Javascript.JSaddle.WebSockets (
1919 jsaddleOr
2020 , jsaddleApp
2121 , jsaddleWithAppOr
22− , jsaddleAppWithJs
23− , jsaddleAppWithJsOr
2422 , jsaddleAppPartial
25− , jsaddleJs
26− , jsaddleJs'
2723 , debug
28− , debugOr
2924 , debugWrapper
3025) where
3126
32−import Control.Monad (when, void, forever)
27+import Control.Monad (when, join, void, forever)
3328import Control.Concurrent (killThread, forkIO, threadDelay)
3429import Control.Exception (handle, AsyncException, throwIO, fromException, finally)
3530
@@ -42,12 +37,12 @@ import Network.Wai
4237import Network.WebSockets
4338 (defaultConnectionOptions, ConnectionOptions(..), sendTextData,
4439 receiveDataMessage, acceptRequest, ServerApp, sendPing)
40+import qualified Network.WebSockets as WS (DataMessage(..))
4541import Network.Wai.Handler.WebSockets (websocketsOr)
46−import Network.HTTP.Types (Status(..))
4742
4843import Language.Javascript.JSaddle.Types (JSM(..), JSContextRef(..))
4944import qualified Network.Wai as W
50− (responseLBS, requestMethod, pathInfo, modifyResponse, responseStatus)
45+ (responseLBS, requestMethod, pathInfo)
5146import qualified Data.Text as T (pack)
5247import qualified Network.HTTP.Types as H
5348 (status403, status200)
@@ -58,59 +53,44 @@ import Language.Javascript.JSaddle.Debug
5853import Data.Maybe (fromMaybe)
5954import qualified Data.Map as M (empty, insert, lookup)
6055import Data.IORef
61− (readIORef, newIORef, atomicModifyIORef')
56+ (writeIORef, IORef, readIORef, newIORef, atomicModifyIORef')
6257import Data.ByteString.Lazy (ByteString)
63−import qualified Data.ByteString.Lazy as LBS
64− (toStrict, stripPrefix)
6558import Control.Concurrent.MVar
66− (tryTakeMVar, MVar, tryPutMVar, modifyMVar_, putMVar, takeMVar,
67− readMVar, newMVar, newEmptyMVar, modifyMVar)
59+ (tryPutMVar, modifyMVar_, putMVar, takeMVar, readMVar, newMVar,
60+ newEmptyMVar, modifyMVar)
6861import Network.Wai.Handler.Warp
6962 (defaultSettings, setTimeout, setPort, runSettings)
70−import Foreign.Store (readStore, lookupStore, writeStore, Store(..))
63+import Foreign.Store (newStore, readStore, lookupStore)
7164import Language.Javascript.JSaddle (askJSM)
7265import Control.Monad.IO.Class (MonadIO(..))
7366
74−import Language.Javascript.JSaddle.WebSockets.Compat (getTextMessageByteString)
75−import qualified Data.Text.Encoding as T (decodeUtf8)
76−
7767jsaddleOr :: ConnectionOptions -> JSM () -> Application -> IO Application
7868jsaddleOr opts entryPoint otherApp = do
7969 syncHandlers <- newIORef M.empty
80− asyncHandlers <- newIORef M.empty
8170 let wsApp :: ServerApp
8271 wsApp pending_conn = do
8372 conn <- acceptRequest pending_conn
84− initMsg <- receiveDataMessage conn
85− case getTextMessageByteString initMsg of
86− Just "" -> do
87− rec (processResult, processSyncResult, start) <- runJavaScript (sendTextData conn . encode) $ do
88− syncKey <- T.pack . show . contextId <$> askJSM
89− 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)
92− entryPoint
93− start
94− waitTillClosed conn
95− Just syncKey ->
96− M.lookup (T.decodeUtf8 $ LBS.toStrict syncKey) <$> readIORef syncHandlers >>= \case
97− Nothing -> error "jsaddle missing sync message handler"
98− Just processResult ->
99− forever $
100− receiveDataMessage conn >>= \msg -> case getTextMessageByteString msg of
101− Just t ->
102− case decode t of
103− Nothing -> error $ "jsaddle Results decode failed : " <> show t
104− Just r -> processResult r
105− _ -> error "jsaddle WebSocket unexpected binary data"
106− _ -> error "jsaddle WebSocket unexpected binary data"
73+ rec (processResult, processSyncResult, start) <- runJavaScript (sendTextData conn . encode) $ do
74+ syncKey <- T.pack . show . contextId <$> askJSM
75+ liftIO $ atomicModifyIORef' syncHandlers (\m -> (M.insert syncKey processSyncResult m, ()))
76+ liftIO $ sendTextData conn (encode syncKey)
77+ entryPoint
78+ _ <- forkIO . forever $
79+ receiveDataMessage conn >>= \case
80+ (WS.Text t) ->
81+ case decode t of
82+ Nothing -> error $ "jsaddle Results decode failed : " <> show t
83+ Just r -> processResult r
84+ _ -> error "jsaddle WebSocket unexpected binary data"
85+ start
86+ waitTillClosed conn
10787
10888 -- Based on Network.WebSocket.forkPingThread
10989 waitTillClosed conn = ignore `handle` go 1
11090 where
11191 go :: Int -> IO ()
11292 go i = do
113− threadDelay (10 * 1000 * 1000)
93+ threadDelay (1 * 1000 * 1000)
11494 sendPing conn (T.pack $ show i)
11595 go (i + 1)
11696
@@ -130,11 +110,7 @@ jsaddleOr opts entryPoint otherApp = do
130110 Just handler -> do
131111 next <- encode <$> handler result
132112 sendResponse $ W.responseLBS H.status200 [("Content-Type", "application/json")] next
133− (method, _) -> (catch404 otherApp) req sendResponse
134− where catch404 = W.modifyResponse $ \resp ->
135− case (method, W.responseStatus resp) of
136− ("GET", Status 404 _) -> indexResponse
137− _ -> resp
113+ _ -> otherApp req sendResponse
138114 return $ websocketsOr opts wsApp syncHandler
139115
140116
@@ -143,14 +119,9 @@ jsaddleApp = jsaddleAppWithJs $ jsaddleJs False
143119
144120jsaddleAppWithJs :: ByteString -> Application
145121jsaddleAppWithJs js req sendResponse =
146− jsaddleAppWithJsOr js
147− (\_ _ -> sendResponse $ W.responseLBS H.status403 [("Content-Type", "text/plain")] "Forbidden")
148− req sendResponse
149−
150−jsaddleAppWithJsOr :: ByteString -> Application -> Application
151−jsaddleAppWithJsOr js otherApp req sendResponse =
152− fromMaybe (otherApp req sendResponse)
153− (jsaddleAppPartialWithJs js req sendResponse)
122+ fromMaybe
123+ (sendResponse $ W.responseLBS H.status403 [("Content-Type", "text/plain")] "Forbidden")
124+ (jsaddleAppPartialWithJs js req sendResponse)
154125
155126jsaddleWithAppOr :: ConnectionOptions -> JSM () -> Application -> IO Application
156127jsaddleWithAppOr opts entryPoint otherApp = jsaddleOr opts entryPoint $ \req sendResponse ->
@@ -160,64 +131,44 @@ jsaddleWithAppOr opts entryPoint otherApp = jsaddleOr opts entryPoint $ \req sen
160131jsaddleAppPartial :: Request -> (Response -> IO ResponseReceived) -> Maybe (IO ResponseReceived)
161132jsaddleAppPartial = jsaddleAppPartialWithJs $ jsaddleJs False
162133
163−indexResponse :: Response
164−indexResponse = W.responseLBS H.status200 [("Content-Type", "text/html")] indexHtml
165−
166134jsaddleAppPartialWithJs :: ByteString -> Request -> (Response -> IO ResponseReceived) -> Maybe (IO ResponseReceived)
167135jsaddleAppPartialWithJs js req sendResponse = case (W.requestMethod req, W.pathInfo req) of
168− ("GET", []) -> Just $ sendResponse indexResponse
136+ ("GET", []) -> Just $ sendResponse $ W.responseLBS H.status200 [("Content-Type", "text/html")] indexHtml
169137 ("GET", ["jsaddle.js"]) -> Just $ sendResponse $ W.responseLBS H.status200 [("Content-Type", "application/javascript")] js
170138 _ -> Nothing
171139
172−jsaddleJs :: Bool -> ByteString
173−jsaddleJs = jsaddleJs' Nothing
174−
175140-- Use this to generate this string for embedding
176141-- sed -e 's|\\|\\\\|g' -e 's|^| \\|' -e 's|$|\\n\\|' -e 's|"|\\"|g' data/jsaddle.js | pbcopy
177−jsaddleJs' :: Maybe ByteString -> Bool -> ByteString
178−jsaddleJs' jsaddleUri refreshOnLoad = "\
179− \if(typeof global !== \"undefined\" && typeof require === \"function\") {\n\
142+jsaddleJs :: Bool -> ByteString
143+jsaddleJs refreshOnLoad = "\
144+ \if(typeof global !== \"undefined\") {\n\
180145 \ global.window = global;\n\
181146 \ global.WebSocket = require('ws');\n\
182147 \}\n\
183148 \\n\
184149 \var connect = function() {\n\
185− \ var wsaddress = "
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")
189− <> ";\n\
150+ \ var wsaddress = window.location.protocol.replace('http', 'ws')+\"//\"+window.location.hostname+(window.location.port?(\":\"+window.location.port):\"\");\n\
190151 \\n\
191− \ var ws0 = new WebSocket(wsaddress);\n\
152+ \ var ws = new WebSocket(wsaddress);\n\
192153 \ var syncKey = \"\";\n\
193154 \\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\
155+ \ ws.onopen = function(e) {\n\
198156 \ " <> initState <> "\n\
199157 \\n\
200− \ ws0.onmessage = function(e) {\n\
158+ \ ws.onmessage = function(e) {\n\
201159 \ var batch = JSON.parse(e.data);\n\
202160 \ if(inCallback > 0) {\n\
203161 \ asyncBatch = batch;\n\
204162 \ return;\n\
205163 \ }\n\
206164 \ if(typeof batch === \"string\") {\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\
213− \ ws = ws1;\n\
214− \ }\n" <>
165+ \ syncKey = batch;\n" <>
215166 (if refreshOnLoad
216167 then " var xhr = new XMLHttpRequest();\n\
217168 \ xhr.open('POST', '/reload/'+syncKey, true);\n\
218169 \ xhr.onreadystatechange = function() {\n\
219170 \ if(xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200)\n\
220− \ setTimeout(function(){window.location.reload();}, 100);\n\
171+ \ setTimeout(function(){window.location.reload();}, 500);\n\
221172 \ };\n\
222173 \ xhr.send();\n"
223174 else "") <>
@@ -227,13 +178,13 @@ jsaddleJs' jsaddleUri refreshOnLoad = "\
227178 \ " <> runBatch (\a -> "ws.send(JSON.stringify(" <> a <> "));")
228179 (Just (\a -> "(function(){\n\
229180 \ var xhr = new XMLHttpRequest();\n\
230− \ xhr.open('POST', '" <> fromMaybe "" jsaddleUri <> "/sync/'+syncKey, false);\n\
181+ \ xhr.open('POST', '/sync/'+syncKey, false);\n\
231182 \ xhr.setRequestHeader(\"Content-type\", \"application/json\");\n\
232183 \ xhr.send(JSON.stringify(" <> a <> "));\n\
233184 \ return JSON.parse(xhr.response);})()")) <> "\
234185 \ };\n\
235186 \ };\n\
236− \ ws0.onerror = function() {\n\
187+ \ ws.onerror = function() {\n\
237188 \ setTimeout(connect, 1000);\n\
238189 \ };\n\
239190 \}\n\
@@ -252,13 +203,6 @@ debug port f = do
252203 jsaddleOr defaultConnectionOptions (registerContext >> f >> syncPoint) (withRefresh $ jsaddleAppWithJs $ jsaddleJs True)
253204 putStrLn $ "<a href=\"http://localhost:" <> show port <> "\">run</a>"
254205
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>"
261−
262206refreshMiddleware :: ((Response -> IO ResponseReceived) -> IO ResponseReceived) -> Middleware
263207refreshMiddleware refresh otherApp req sendResponse = case (W.requestMethod req, W.pathInfo req) of
264208 ("POST", ["reload", _syncKey]) -> refresh sendResponse
@@ -296,24 +240,15 @@ debugWrapper run = do
296240 ctxs <- takeMVar contexts
297241 mapM_ removeContext ctxs
298242 takeMVar reloadDoneMVars >>= mapM_ takeMVar
299− tryTakeMVar serverDone >>= \case
300− Nothing -> do
301− killThread thread
302− takeMVar serverDone
303− Just _ -> return ()
243+ killThread thread
244+ takeMVar serverDone
304245 return $ length ctxs
305− restarter :: MVar (Int -> IO (IO Int)) -> IO Int -> IO ()
306− restarter mvar stop = do
307− start' <- takeMVar mvar
308− n <- stop
309− start' n >>= restarter mvar
310− lookupStore storeId >>= \case
246+ lookupStore shutdown_0 >>= \case
311247 Nothing -> do
312− restartMVar <- newMVar start
313− void . forkIO $ restarter restartMVar (return 0)
314− void $ writeStore (Store storeId) restartMVar
248+ shutdownRef <- newIORef =<< start 0
249+ void $ newStore shutdownRef
315250 Just shutdownStore -> do
316− restartMVar :: MVar (Int -> IO (IO Int)) <- readStore shutdownStore
317− void $ tryTakeMVar restartMVar
318− putMVar restartMVar start
319− where storeId = 354
251+ shutdownRef :: IORef (IO Int) <- readStore shutdownStore
252+ expectedConnections <- join (readIORef shutdownRef)
253+ writeIORef shutdownRef =<< start expectedConnections
254+ where shutdown_0 = 0