packages feed

jsaddle-warp 0.9.0.0 → 0.9.1.0

raw patch · 3 files changed

+129/−49 lines, 3 filesdep +uuiddep +uuid-typesdep ~bytestringPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: uuid, uuid-types

Dependency ranges changed: bytestring

API changes (from Hackage documentation)

- Language.Javascript.JSaddle.Warp: jsaddleApp :: Application
- Language.Javascript.JSaddle.Warp: jsaddleOr :: ConnectionOptions -> JSM () -> Application -> Application
+ Language.Javascript.JSaddle.WebSockets: jsaddleAppPartial :: Request -> (Response -> IO ResponseReceived) -> Maybe (IO ResponseReceived)
+ Language.Javascript.JSaddle.WebSockets: jsaddleWithAppOr :: ConnectionOptions -> JSM () -> Application -> IO Application
- Language.Javascript.JSaddle.WebSockets: jsaddleOr :: ConnectionOptions -> JSM () -> Application -> Application
+ Language.Javascript.JSaddle.WebSockets: jsaddleOr :: ConnectionOptions -> JSM () -> Application -> IO Application

Files

jsaddle-warp.cabal view
@@ -1,5 +1,5 @@ name: jsaddle-warp-version: 0.9.0.0+version: 0.9.1.0 cabal-version: >=1.10 build-type: Simple license: MIT@@ -26,6 +26,7 @@             Language.Javascript.JSaddle.WebSockets         build-depends:             aeson >=0.8.0.2 && <1.3,+            bytestring >=0.10.6.0 && <0.11,             containers >=0.5.6.2 && <0.6,             http-types >=0.8.6 && <0.10,             jsaddle >=0.9.0.0 && <0.10,@@ -33,6 +34,8 @@             text >=1.2.1.3 && <1.3,             time >=1.5.0.1 && <1.8,             transformers >=0.4.2.0 && <0.6,+            uuid >=1.3.13 && <1.4,+            uuid-types >=1.0.3 && <1.1,             wai >=3.0.3.0 && <3.3,             wai-websockets >=3.0.0.6 && <3.1,             warp >=3.1.2 && <3.3,
src/Language/Javascript/JSaddle/Warp.hs view
@@ -17,8 +17,7 @@   -- * Running JSM over WebSockets     run #ifndef ghcjs_HOST_OS-  , jsaddleOr-  , jsaddleApp+  , module Language.Javascript.JSaddle.WebSockets #endif ) where @@ -29,7 +28,7 @@  import Language.Javascript.JSaddle.Types (JSM) import Language.Javascript.JSaddle.Run (syncPoint)-import Language.Javascript.JSaddle.WebSockets (jsaddleOr, jsaddleApp)+import Language.Javascript.JSaddle.WebSockets #endif  -- | Run the given 'JSM' action as the main entry point.  Either directly@@ -40,6 +39,6 @@ #else run :: Int -> JSM () -> IO () run port f =-    runSettings (setPort port (setTimeout 3600 defaultSettings)) $+    runSettings (setPort port (setTimeout 3600 defaultSettings)) =<<         jsaddleOr defaultConnectionOptions (f >> syncPoint) jsaddleApp #endif
src/Language/Javascript/JSaddle/WebSockets.hs view
@@ -16,6 +16,8 @@   -- * Running JSM over WebSockets     jsaddleOr   , jsaddleApp+  , jsaddleWithAppOr+  , jsaddleAppPartial ) where  import Control.Monad (forever)@@ -25,7 +27,8 @@ import Data.Monoid ((<>)) import Data.Aeson (encode, decode) -import Network.Wai (Application)+import Network.Wai+       (lazyRequestBody, Application, Request, Response, ResponseReceived) import Network.WebSockets        (ConnectionOptions(..), sendTextData, receiveDataMessage,         acceptRequest, ServerApp, sendPing)@@ -34,55 +37,130 @@  import Language.Javascript.JSaddle.Types (JSM(..)) import qualified Network.Wai as W-       (responseLBS, requestMethod, Application, pathInfo)-import Data.Text (Text)+       (responseLBS, requestMethod, pathInfo) import qualified Data.Text as T (pack) import qualified Network.HTTP.Types as H-       (status403, status200, status405)+       (status403, status200) import Language.Javascript.JSaddle.Run (runJavaScript)-import Language.Javascript.JSaddle.Run.Files (indexHtml, jsaddleJs)+import Language.Javascript.JSaddle.Run.Files (indexHtml, runBatch, ghcjsHelpers, initState)+import Data.Maybe (fromMaybe)+import qualified Data.Map as M (empty, insert, lookup)+import Data.IORef (readIORef, newIORef, atomicModifyIORef')+import Data.ByteString.Lazy (fromStrict, ByteString)+import Data.Text (Text)+import Data.UUID.Types (toText)+import Data.UUID.V4 (nextRandom) -jsaddleOr :: ConnectionOptions -> JSM () -> Application -> Application-jsaddleOr opts entryPoint = websocketsOr opts wsApp-  where-    wsApp :: ServerApp-    wsApp pending_conn = do-        conn <- acceptRequest pending_conn-        (processResult, _, start) <- runJavaScript (sendTextData conn . encode) entryPoint-        _ <- forkIO . forever $-            receiveDataMessage conn >>= \case-                (WS.Text t) ->-                    case decode t of-                        Nothing -> error $ "jsaddle Results decode failed : " <> show t-                        Just r  -> processResult r-                _ -> error "jsaddle WebSocket unexpected binary data"-        start-        waitTillClosed conn+jsaddleOr :: ConnectionOptions -> JSM () -> Application -> IO Application+jsaddleOr opts entryPoint otherApp = do+    syncHandlers <- newIORef M.empty+    let wsApp :: ServerApp+        wsApp pending_conn = do+            conn <- acceptRequest pending_conn+            (processResult, processSyncResult, start) <- runJavaScript (sendTextData conn . encode) entryPoint+            _ <- forkIO . forever $+                receiveDataMessage conn >>= \case+                    (WS.Text t) ->+                        case decode t of+                            Nothing -> error $ "jsaddle Results decode failed : " <> show t+                            Just r  -> processResult r+                    _ -> error "jsaddle WebSocket unexpected binary data"+            syncKey <- toText <$> nextRandom+            atomicModifyIORef' syncHandlers (\m -> (M.insert syncKey processSyncResult m, ()))+            sendTextData conn (encode syncKey)+            start+            waitTillClosed conn -    -- Based on Network.WebSocket.forkPingThread-    waitTillClosed conn = ignore `handle` go 1-      where-        go :: Int -> IO ()-        go i = do-            threadDelay (1 * 1000 * 1000)-            sendPing conn (T.pack $ show i)-            go (i + 1)+        -- Based on Network.WebSocket.forkPingThread+        waitTillClosed conn = ignore `handle` go 1+          where+            go :: Int -> IO ()+            go i = do+                threadDelay (1 * 1000 * 1000)+                sendPing conn (T.pack $ show i)+                go (i + 1) -    ignore e = case fromException e of-        Just async -> throwIO (async :: AsyncException)-        Nothing    -> return ()+        ignore e = case fromException e of+            Just async -> throwIO (async :: AsyncException)+            Nothing    -> return () +        syncHandler :: Application+        syncHandler req sendResponse = case (W.requestMethod req, W.pathInfo req) of+            ("POST", ["sync", syncKey]) -> do+                body <- lazyRequestBody req+                case decode body of+                    Nothing -> error $ "jsaddle sync message decode failed : " <> show body+                    Just result ->+                        M.lookup syncKey <$> readIORef syncHandlers >>= \case+                            Nothing -> error "jsaddle missing sync message handler"+                            Just handler -> do+                                next <- encode <$> handler result+                                sendResponse $ W.responseLBS H.status200 [("Content-Type", "application/json")] next+            _ -> otherApp req sendResponse+    return $ websocketsOr opts wsApp syncHandler++ jsaddleApp :: Application-jsaddleApp req = jsaddleAppPieces (W.pathInfo req) req+jsaddleApp req sendResponse =+    fromMaybe+        (sendResponse $  W.responseLBS H.status403 [("Content-Type", "text/plain")] "Forbidden")+        (jsaddleAppPartial req sendResponse) -jsaddleAppPieces :: [Text] -> W.Application-jsaddleAppPieces _ req sendResponse-    | W.requestMethod req `notElem` ["GET", "HEAD"] = sendResponse $ W.responseLBS-        H.status405-        [("Content-Type", "text/plain")]-        "Only GET or HEAD is supported"-jsaddleAppPieces [] _req sendResponse = sendResponse $ W.responseLBS H.status200 [("Content-Type", "text/html")] indexHtml-jsaddleAppPieces ["jsaddle.js"] _req sendResponse = sendResponse $ W.responseLBS H.status200 [("Content-Type", "application/javascript")] jsaddleJs-jsaddleAppPieces _rawPieces _req sendResponse = sendResponse $ W.responseLBS H.status403-            [ ("Content-Type", "text/plain")-            ] "Forbidden"+jsaddleWithAppOr :: ConnectionOptions -> JSM () -> Application -> IO Application+jsaddleWithAppOr opts entryPoint otherApp = jsaddleOr opts entryPoint $ \req sendResponse ->+  (fromMaybe (otherApp req sendResponse)+     (jsaddleAppPartial req sendResponse))++jsaddleAppPartial :: Request -> (Response -> IO ResponseReceived) -> Maybe (IO ResponseReceived)+jsaddleAppPartial req sendResponse = case (W.requestMethod req, W.pathInfo req) of+    ("GET", []) -> Just $ sendResponse $ W.responseLBS H.status200 [("Content-Type", "text/html")] indexHtml+    ("GET", ["jsaddle.js"]) -> Just $ sendResponse $ W.responseLBS H.status200 [("Content-Type", "application/javascript")] jsaddleJs+    _ -> Nothing++-- Use this to generate this string for embedding+-- sed -e 's|\\|\\\\|g' -e 's|^|    \\|' -e 's|$|\\n\\|' -e 's|"|\\"|g' data/jsaddle.js | pbcopy+jsaddleJs :: ByteString+jsaddleJs = "\+    \if(typeof global !== \"undefined\") {\n\+    \    global.window = global;\n\+    \    global.WebSocket = require('ws');\n\+    \}\n\+    \\n\+    \var connect = function() {\n\+    \    var wsaddress = window.location.protocol.replace('http', 'ws')+\"//\"+window.location.hostname+(window.location.port?(\":\"+window.location.port):\"\");\n\+    \\n\+    \    var ws = new WebSocket(wsaddress);\n\+    \    var syncKey = \"\";\n\+    \\n\+    \    ws.onopen = function(e) {\n\+    \ " <> initState <> "\n\+    \        var inXhrSend = false;\n\+    \\n\+    \        ws.onmessage = function(e) {\n\+    \            if(inXhrSend) return;\n\+    \            var batch = JSON.parse(e.data);\n\+    \            if(typeof batch === \"string\") {\n\+    \                syncKey = batch;\n\+    \                return;\n\+    \            }\n\+    \\n\+    \ " <> runBatch (\a -> "ws.send(JSON.stringify(" <> a <> "));")+              (Just (\a -> "(function(){\n\+                  \                       inXhrSend = true;\n\+                  \                       var xhr = new XMLHttpRequest();\n\+                  \                       xhr.open('POST', '/sync/'+syncKey, false);\n\+                  \                       xhr.setRequestHeader(\"Content-type\", \"application/json\");\n\+                  \                       xhr.send(JSON.stringify(" <> a <> "));\n\+                  \                       inXhrSend = false;\n\+                  \                       return JSON.parse(xhr.response);})()")) <> "\+    \        };\n\+    \    };\n\+    \    ws.onerror = function() {\n\+    \        setTimeout(connect, 1000);\n\+    \    };\n\+    \}\n\+    \\n\+    \ " <> ghcjsHelpers <> "\+    \connect();\n\+    \"+