@@ -19,9 +19,13 @@ module Language.Javascript.JSaddle.WebSockets (
20 , jsaddleApp
20 , jsaddleApp
21 , jsaddleWithAppOr
21 , jsaddleWithAppOr
22 , jsaddleAppPartial
24 , jsaddleAppPartial
23 , jsaddleJs
25 , jsaddleJs
25 , debugWrapper
29 , debugWrapper
@@ -38,7 +42,6 @@ import Network.Wai
38import Network.WebSockets
42import Network.WebSockets
39 (defaultConnectionOptions, ConnectionOptions(..), sendTextData,
43 (defaultConnectionOptions, ConnectionOptions(..), sendTextData,
40 receiveDataMessage, acceptRequest, ServerApp, sendPing)
44 receiveDataMessage, acceptRequest, ServerApp, sendPing)
41−import qualified Network.WebSockets as WS (DataMessage(..))
42import Network.Wai.Handler.WebSockets (websocketsOr)
45import Network.Wai.Handler.WebSockets (websocketsOr)
43import Network.HTTP.Types (Status(..))
46import Network.HTTP.Types (Status(..))
@@ -57,42 +60,57 @@ import qualified Data.Map as M (empty, insert, lookup)
57import Data.IORef
60import Data.IORef
58 (readIORef, newIORef, atomicModifyIORef')
61 (readIORef, newIORef, atomicModifyIORef')
59import Data.ByteString.Lazy (ByteString)
62import Data.ByteString.Lazy (ByteString)
63+import qualified Data.ByteString.Lazy as LBS
64+ (toStrict, stripPrefix)
60import Control.Concurrent.MVar
65import Control.Concurrent.MVar
61 (tryTakeMVar, MVar, tryPutMVar, modifyMVar_, putMVar, takeMVar,
66 (tryTakeMVar, MVar, tryPutMVar, modifyMVar_, putMVar, takeMVar,
62 readMVar, newMVar, newEmptyMVar, modifyMVar)
67 readMVar, newMVar, newEmptyMVar, modifyMVar)
63import Network.Wai.Handler.Warp
68import Network.Wai.Handler.Warp
64 (defaultSettings, setTimeout, setPort, runSettings)
69 (defaultSettings, setTimeout, setPort, runSettings)
65−import Foreign.Store (newStore, readStore, lookupStore)
70+import Foreign.Store (readStore, lookupStore, writeStore, Store(..))
66import Language.Javascript.JSaddle (askJSM)
71import Language.Javascript.JSaddle (askJSM)
67import Control.Monad.IO.Class (MonadIO(..))
72import Control.Monad.IO.Class (MonadIO(..))
74+import Language.Javascript.JSaddle.WebSockets.Compat (getTextMessageByteString)
75+import qualified Data.Text.Encoding as T (decodeUtf8)
69jsaddleOr :: ConnectionOptions -> JSM () -> Application -> IO Application
77jsaddleOr :: ConnectionOptions -> JSM () -> Application -> IO Application
70jsaddleOr opts entryPoint otherApp = do
78jsaddleOr opts entryPoint otherApp = do
71 syncHandlers <- newIORef M.empty
79 syncHandlers <- newIORef M.empty
80+ asyncHandlers <- newIORef M.empty
72 let wsApp :: ServerApp
81 let wsApp :: ServerApp
73 wsApp pending_conn = do
82 wsApp pending_conn = do
74 conn <- acceptRequest pending_conn
83 conn <- acceptRequest pending_conn
84+ initMsg <- receiveDataMessage conn
85+ case getTextMessageByteString initMsg of
75− rec (processResult, processSyncResult, start) <- runJavaScript (sendTextData conn . encode) $ do
87+ rec (processResult, processSyncResult, start) <- runJavaScript (sendTextData conn . encode) $ do
76− syncKey <- T.pack . show . contextId <$> askJSM
88+ syncKey <- T.pack . show . contextId <$> askJSM
77− liftIO $ atomicModifyIORef' syncHandlers (\m -> (M.insert syncKey processSyncResult m, ()))
89+ liftIO $ atomicModifyIORef' syncHandlers (\m -> (M.insert syncKey processSyncResult m, ()))
90+ liftIO $ atomicModifyIORef' asyncHandlers (\m -> (M.insert syncKey processResult m, ()))
78− liftIO $ sendTextData conn (encode syncKey)
91+ liftIO $ sendTextData conn (encode syncKey)
79− entryPoint
92+ entryPoint
80− _ <- forkIO . forever $
93+ start
81− receiveDataMessage conn >>= \case
94+ waitTillClosed conn
82− (WS.Text t) ->
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 ->
100+ receiveDataMessage conn >>= \msg -> case getTextMessageByteString msg of
83− case decode t of
102+ case decode t of
84− Nothing -> error $ "jsaddle Results decode failed : " <> show t
103+ Nothing -> error $ "jsaddle Results decode failed : " <> show t
85− Just r -> processResult r
104+ Just r -> processResult r
86− _ -> error "jsaddle WebSocket unexpected binary data"
105+ _ -> error "jsaddle WebSocket unexpected binary data"
87− start
106+ _ -> error "jsaddle WebSocket unexpected binary data"
90 -- Based on Network.WebSocket.forkPingThread
108 -- Based on Network.WebSocket.forkPingThread
91 waitTillClosed conn = ignore `handle` go 1
109 waitTillClosed conn = ignore `handle` go 1
93 go :: Int -> IO ()
111 go :: Int -> IO ()
94 go i = do
112 go i = do
95− threadDelay (1 * 1000 * 1000)
113+ threadDelay (10 * 1000 * 1000)
96 sendPing conn (T.pack $ show i)
114 sendPing conn (T.pack $ show i)
97 go (i + 1)
115 go (i + 1)
@@ -125,9 +143,14 @@ jsaddleApp = jsaddleAppWithJs $ jsaddleJs False
126jsaddleAppWithJs :: ByteString -> Application
144jsaddleAppWithJs :: ByteString -> Application
127jsaddleAppWithJs js req sendResponse =
145jsaddleAppWithJs js req sendResponse =
128− fromMaybe
146+ jsaddleAppWithJsOr js
129− (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)
130− (jsaddleAppPartialWithJs js req sendResponse)
153+ (jsaddleAppPartialWithJs js req sendResponse)
132jsaddleWithAppOr :: ConnectionOptions -> JSM () -> Application -> IO Application
155jsaddleWithAppOr :: ConnectionOptions -> JSM () -> Application -> IO Application
133jsaddleWithAppOr opts entryPoint otherApp = jsaddleOr opts entryPoint $ \req sendResponse ->
156jsaddleWithAppOr opts entryPoint otherApp = jsaddleOr opts entryPoint $ \req sendResponse ->
@@ -146,32 +169,49 @@ jsaddleAppPartialWithJs js req sendResponse = case (W.requestMethod req, W.pathI
146 ("GET", ["jsaddle.js"]) -> Just $ sendResponse $ W.responseLBS H.status200 [("Content-Type", "application/javascript")] js
169 ("GET", ["jsaddle.js"]) -> Just $ sendResponse $ W.responseLBS H.status200 [("Content-Type", "application/javascript")] js
147 _ -> Nothing
170 _ -> Nothing
172+jsaddleJs :: Bool -> ByteString
173+jsaddleJs = jsaddleJs' Nothing
149-- Use this to generate this string for embedding
175-- Use this to generate this string for embedding
150-- 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
151−jsaddleJs :: Bool -> ByteString
177+jsaddleJs' :: Maybe ByteString -> Bool -> ByteString
152−jsaddleJs refreshOnLoad = "\
178+jsaddleJs' jsaddleUri refreshOnLoad = "\
153− \if(typeof global !== \"undefined\") {\n\
179+ \if(typeof global !== \"undefined\" && typeof require === \"function\") {\n\
154 \ global.window = global;\n\
180 \ global.window = global;\n\
155 \ global.WebSocket = require('ws');\n\
181 \ global.WebSocket = require('ws');\n\
158 \var connect = function() {\n\
184 \var connect = function() {\n\
159− \ 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")
161− \ var ws = new WebSocket(wsaddress);\n\
191+ \ var ws0 = new WebSocket(wsaddress);\n\
162 \ var syncKey = \"\";\n\
192 \ var syncKey = \"\";\n\
164− \ 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\
165 \ " <> initState <> "\n\
198 \ " <> initState <> "\n\
167− \ ws.onmessage = function(e) {\n\
200+ \ ws0.onmessage = function(e) {\n\
168 \ var batch = JSON.parse(e.data);\n\
201 \ var batch = JSON.parse(e.data);\n\
169 \ if(inCallback > 0) {\n\
202 \ if(inCallback > 0) {\n\
170 \ asyncBatch = batch;\n\
203 \ asyncBatch = batch;\n\
171 \ return;\n\
204 \ return;\n\
173 \ if(typeof batch === \"string\") {\n\
206 \ if(typeof batch === \"string\") {\n\
174− \ 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\
175 (if refreshOnLoad
215 (if refreshOnLoad
176 then " var xhr = new XMLHttpRequest();\n\
216 then " var xhr = new XMLHttpRequest();\n\
177 \ xhr.open('POST', '/reload/'+syncKey, true);\n\
217 \ xhr.open('POST', '/reload/'+syncKey, true);\n\
@@ -187,13 +227,13 @@ jsaddleJs refreshOnLoad = "\
187 \ " <> runBatch (\a -> "ws.send(JSON.stringify(" <> a <> "));")
227 \ " <> runBatch (\a -> "ws.send(JSON.stringify(" <> a <> "));")
188 (Just (\a -> "(function(){\n\
228 (Just (\a -> "(function(){\n\
189 \ var xhr = new XMLHttpRequest();\n\
229 \ var xhr = new XMLHttpRequest();\n\
190− \ xhr.open('POST', '/sync/'+syncKey, false);\n\
230+ \ xhr.open('POST', '" <> fromMaybe "" jsaddleUri <> "/sync/'+syncKey, false);\n\
191 \ xhr.setRequestHeader(\"Content-type\", \"application/json\");\n\
231 \ xhr.setRequestHeader(\"Content-type\", \"application/json\");\n\
192 \ xhr.send(JSON.stringify(" <> a <> "));\n\
232 \ xhr.send(JSON.stringify(" <> a <> "));\n\
193 \ return JSON.parse(xhr.response);})()")) <> "\
233 \ return JSON.parse(xhr.response);})()")) <> "\
196− \ ws.onerror = function() {\n\
236+ \ ws0.onerror = function() {\n\
197 \ setTimeout(connect, 1000);\n\
237 \ setTimeout(connect, 1000);\n\
@@ -212,6 +252,13 @@ debug port f = do
212 jsaddleOr defaultConnectionOptions (registerContext >> f >> syncPoint) (withRefresh $ jsaddleAppWithJs $ jsaddleJs True)
252 jsaddleOr defaultConnectionOptions (registerContext >> f >> syncPoint) (withRefresh $ jsaddleAppWithJs $ jsaddleJs True)
213 putStrLn $ "<a href=\"http://localhost:" <> show port <> "\">run</a>"
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>"
215refreshMiddleware :: ((Response -> IO ResponseReceived) -> IO ResponseReceived) -> Middleware
262refreshMiddleware :: ((Response -> IO ResponseReceived) -> IO ResponseReceived) -> Middleware
216refreshMiddleware refresh otherApp req sendResponse = case (W.requestMethod req, W.pathInfo req) of
263refreshMiddleware refresh otherApp req sendResponse = case (W.requestMethod req, W.pathInfo req) of
217 ("POST", ["reload", _syncKey]) -> refresh sendResponse
264 ("POST", ["reload", _syncKey]) -> refresh sendResponse
@@ -260,13 +307,13 @@ debugWrapper run = do
260 start' <- takeMVar mvar
307 start' <- takeMVar mvar
261 n <- stop
308 n <- stop
262 start' n >>= restarter mvar
309 start' n >>= restarter mvar
263− lookupStore shutdown_0 >>= \case
310+ lookupStore storeId >>= \case
264 Nothing -> do
311 Nothing -> do
265 restartMVar <- newMVar start
312 restartMVar <- newMVar start
266 void . forkIO $ restarter restartMVar (return 0)
313 void . forkIO $ restarter restartMVar (return 0)
267− void $ newStore restartMVar
314+ void $ writeStore (Store storeId) restartMVar
268 Just shutdownStore -> do
315 Just shutdownStore -> do
269 restartMVar :: MVar (Int -> IO (IO Int)) <- readStore shutdownStore
316 restartMVar :: MVar (Int -> IO (IO Int)) <- readStore shutdownStore
270 void $ tryTakeMVar restartMVar
317 void $ tryTakeMVar restartMVar
271 putMVar restartMVar start
318 putMVar restartMVar start
272− where shutdown_0 = 0
319+ where storeId = 354