packages feed

kansas-comet 0.2 → 0.3.0

raw patch · 4 files changed

+286/−233 lines, 4 filesdep ~aesondep ~basedep ~scottyPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: aeson, base, scotty, stm, text, transformers, unordered-containers

API changes (from Hackage documentation)

- Web.KansasComet: Options :: String -> Int -> Options
- Web.KansasComet: connect :: Options -> (Document -> IO ()) -> ScottyM ()
- Web.KansasComet: data Document
- Web.KansasComet: data Options
- Web.KansasComet: debugDocument :: IO Document
- Web.KansasComet: debugReplyDocument :: Document -> Int -> Value -> IO ()
- Web.KansasComet: getReply :: Document -> Int -> IO Value
- Web.KansasComet: instance Default Options
- Web.KansasComet: kCometPlugin :: IO String
- Web.KansasComet: prefix :: Options -> String
- Web.KansasComet: send :: Document -> String -> IO ()
- Web.KansasComet: verbose :: Options -> Int
+ Web.Scotty.Comet: Options :: String -> Int -> Options
+ Web.Scotty.Comet: connect :: Options -> (Document -> IO ()) -> ScottyM ()
+ Web.Scotty.Comet: data Document
+ Web.Scotty.Comet: data Options
+ Web.Scotty.Comet: debugDocument :: IO Document
+ Web.Scotty.Comet: debugReplyDocument :: Document -> Int -> Value -> IO ()
+ Web.Scotty.Comet: defaultOptions :: Options
+ Web.Scotty.Comet: eventQueue :: Document -> TChan Value
+ Web.Scotty.Comet: getReply :: Document -> Int -> IO Value
+ Web.Scotty.Comet: instance Default Options
+ Web.Scotty.Comet: kCometPlugin :: IO String
+ Web.Scotty.Comet: prefix :: Options -> String
+ Web.Scotty.Comet: send :: Document -> Text -> IO ()
+ Web.Scotty.Comet: verbose :: Options -> Int

Files

− Web/KansasComet.hs
@@ -1,217 +0,0 @@-{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, KindSignatures, GADTs #-}-module Web.KansasComet-    ( connect-    , kCometPlugin-    , send-    , Document-    , Options(..)-    , getReply-    , debugDocument-    , debugReplyDocument-    ) where--import Web.Scotty (ScottyM, text, post, capture, param, header, get, ActionM, jsonData)-import Data.Aeson hiding ((.=))-import Control.Monad-import Control.Concurrent.STM as STM-import Control.Concurrent.MVar as STM-import Control.Monad.IO.Class-import Paths_kansas_comet-import qualified Data.Map as Map-import Control.Concurrent-import Data.Default-import Data.Maybe ( fromJust )-import qualified Data.HashMap.Strict as HashMap--import qualified Data.Text.Lazy as LT-import qualified Data.Text      as T-import Data.Time.Calendar-import Data.Time.Clock-import Numeric---- | connect "/foobar" (...) gives a scotty session that:------ >  POST http://.../foobar/                       <- bootstrap the interaction--- >  GET  http://.../foobar/act/<id#>/<act#>       <- get a specific action--- >  POST http://.../foobar/reply/<id#>/<reply#>   <- send a reply as a JSON object--connect :: Options             -- ^ URL path prefix for this page-        -> (Document -> IO ()) -- ^ called for access of the page-        -> ScottyM ()-connect opt callback = do-   when (verbose opt >= 1) $ liftIO $ putStrLn $ "kansas-comet connect with prefix=" ++ show (prefix opt)--   -- A unique number generator, or ephemeral generator.-   -- This is the (open) secret between the client and server.-   -- (Why are we using an MVar vs a TMVar? No specific reason here)-   uniqVar <- liftIO $ newMVar 0-   let getUniq :: IO Int-       getUniq = do-              u <- takeMVar uniqVar-              putMVar uniqVar (u + 1)-              return u--   tm ::  UTCTime  <- liftIO $ getCurrentTime--   let server_id-           = Numeric.showHex (toModifiedJulianDay (utctDay tm))-           $ ("-" ++)-           $ Numeric.showHex (floor (utctDayTime tm * 1000) :: Integer)-           $ ""--   contextDB <- liftIO $ atomically $ newTVar $ (Map.empty :: Map.Map Int Document)-   let newContext :: IO Int-       newContext = do-            uq <- getUniq-            picture <- atomically $ newEmptyTMVar-            callbacks <- atomically $ newTVar $ Map.empty-            let cxt = Document picture callbacks uq-            liftIO $ atomically $ do-                    db <- readTVar contextDB-                    -- assumes the getUniq is actually unique-                    writeTVar contextDB $ Map.insert uq cxt db-            -- Here is where we actually spawn the user code-            _ <- forkIO $ callback cxt-            return uq--   -- POST starts things off.-   post (capture $ prefix opt ++ "/") $ do-            uq  <- liftIO $ newContext-            text (LT.pack $ "$.kc.session(" ++ show server_id ++ "," ++ show uq ++ ");")--   -- GET the updates to the documents (should this be an (empty) POST?)----   liftIO $ print $ prefix opt ++ "/act/:id/:act"-   get (capture $ prefix opt ++ "/act/" ++ server_id ++ "/:id/:act") $ do-            header "Cache-Control" "max-age=0, no-cache, private, no-store, must-revalidate"-            -- do something and return a new list of commands to the client-            num <- param "id"--            when (verbose opt >= 2) $ liftIO $ putStrLn $-                "Kansas Comet: get .../act/" ++ show num---            liftIO $ print (num :: Int)--            let tryPushAction :: TMVar T.Text -> Int -> ActionM ()-                tryPushAction var n = do-                    -- The PUSH archtecture means that we wait upto 3 seconds if there-                    -- is not javascript to push yet. This stops a busy-waiting-                    -- (or technically restricts it to once every 3 second busy)-                    ping <- liftIO $ registerDelay (3 * 1000 * 1000)-                    res <- liftIO $ atomically $ do-                            b <- readTVar ping-                            if b then return Nothing else do-                                 liftM Just (takeTMVar var)---                    when (verbose opt >= 2) $ liftIO $ putStrLn $-                                "Kansas Comet (sending to " ++ show n ++ "):\n" ++ show res--                    case res of-                     Just js -> do---                            liftIO $ putStrLn $ show js-                            text $ LT.pack $ T.unpack js-                     Nothing  ->-                            -- give the browser something to do (approx every 3 seconds)-                            text (LT.pack "")--            db <- liftIO $ atomically $ readTVar contextDB-            case Map.lookup num db of-               Nothing  -> text (LT.pack $ "console.warn('Can not find act #" ++ show num ++ "');")-               Just doc -> tryPushAction (sending doc) num---   post (capture $ prefix opt ++ "/reply/" ++ server_id ++ "/:id/:uq") $ do-           header "Cache-Control" "max-age=0, no-cache, private, no-store, must-revalidate"-           num <- param "id"-           uq :: Int <- param "uq"-           --liftIO $ print (num :: Int, event :: String)--           when (verbose opt >= 2) $ liftIO $ putStrLn $-                "Kansas Comet: post .../reply/" ++ show num ++ "/" ++ show uq--           wrappedVal :: Value <- jsonData-           -- Unwrap the data wrapped, because 'jsonData' only supports-           -- objects or arrays, but not primitive values like numbers-           -- or booleans.-           let val = fromJust $ let (Object m) = wrappedVal-                                in HashMap.lookup (T.pack "data") m-           --liftIO $ print (val :: Value)-           db <- liftIO $ atomically $ readTVar contextDB-           case Map.lookup num db of-               Nothing  -> do-                   text (LT.pack $ "console.warn('Ignore reply for session #" ++ show num ++ "');")-               Just doc -> do-                   liftIO $ do-                         atomically $ do-                           m <- readTVar (listening doc)-                           writeTVar (listening doc) $ Map.insert uq val m-                   text $ LT.pack ""--   return ()---- | 'kCometPlugin' provides the location of the Kansas Comet jQuery plugin.-kCometPlugin :: IO String-kCometPlugin = do-        dataDir <- getDataDir-        return $ dataDir ++ "/static/js/kansas-comet.js"---- | 'send' sends a javascript fragement to a document.--- The string argument will be evaluated before sending (in case there is an error,--- or some costly evaluation needs done first).--- 'send' suspends the thread if the last javascript has not been *dispatched*--- the the browser.-send :: Document -> String -> IO ()-send doc js = atomically $ putTMVar (sending doc) $! T.pack js---- | wait for a virtual-to-this-document's port numbers' reply.-getReply :: Document -> Int -> IO Value-getReply doc num = do-        atomically $ do-           db <- readTVar (listening doc)-           case Map.lookup num db of-              Nothing -> retry-              Just r -> do-                      writeTVar (listening doc) $ Map.delete num db-                      return r----- | 'Document' is the Handle into a specific interaction with a web page.-data Document = Document-        { sending   :: TMVar T.Text             -- ^ Code to be sent to the browser-                                                -- This is a TMVar to stop the generation-                                                -- getting ahead of the rendering engine-        , listening :: TVar (Map.Map Int Value) -- ^ This is numbered replies.-        , _secret    :: Int                      -- ^ the (session) number of this document-        }---- 'Options' for Comet.-data Options = Options-        { prefix  :: String             -- ^ what is the prefix at at start of the URL (for example \"ajax\")-        , verbose :: Int                -- ^ 0 == none, 1 == inits, 2 == cmds done, 3 == complete log-        }--instance Default Options where-  def = Options-        { prefix = ""                   -- default to root, this assumes single page, etc.-        , verbose = 1-        }------------------------------------------------------------------------------------------- | Generate a @Document@ that prints what is would send to the server.-debugDocument :: IO Document-debugDocument = do-  picture <- atomically $ newEmptyTMVar-  callbacks <- atomically $ newTVar $ Map.empty-  _ <- forkIO $ forever $ do-          res <- atomically $ takeTMVar $ picture-          putStrLn $ "Sending: " ++ show res-  return $ Document picture callbacks 0---- | Fake a specific reply on a virtual @Document@ port.-debugReplyDocument :: Document -> Int -> Value -> IO ()-debugReplyDocument doc uq val = atomically $ do-   m <- readTVar (listening doc)-   writeTVar (listening doc) $ Map.insert uq val m-
+ Web/Scotty/Comet.hs view
@@ -0,0 +1,256 @@+{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, KindSignatures, GADTs #-}+module Web.Scotty.Comet+    ( connect+    , kCometPlugin+    , send+    , Document+    , Options(..)+    , getReply+    , eventQueue+    , debugDocument+    , debugReplyDocument+    , defaultOptions+    ) where++import Web.Scotty (ScottyM, text, post, capture, param, setHeader, get, ActionM, jsonData)+import Data.Aeson hiding ((.=))+import Control.Monad+import Control.Concurrent.STM as STM+import Control.Concurrent.MVar as STM+import Control.Monad.IO.Class+import Paths_kansas_comet+import qualified Data.Map as Map+import Control.Concurrent+import Data.Default+import Data.Maybe ( fromJust )+import qualified Data.HashMap.Strict as HashMap+import System.Exit+import qualified Data.Text.Lazy as LT+import qualified Data.Text      as T+import Data.Time.Calendar+import Data.Time.Clock+import Numeric++-- | connect "/foobar" (...) gives a scotty session that:+--+-- >  POST http://.../foobar/                       <- bootstrap the interaction+-- >  GET  http://.../foobar/act/<id#>/<act#>       <- get a specific action+-- >  POST http://.../foobar/reply/<id#>/<reply#>   <- send a reply as a JSON object++connect :: Options             -- ^ URL path prefix for this page+        -> (Document -> IO ()) -- ^ called for access of the page+        -> ScottyM ()+connect opt callback = do+   if not rtsSupportsBoundThreads  -- we need the -threaded flag turned on+   then liftIO $ do putStrLn "Application needs to be re-compiled with -threaded flag"+                    exitFailure+   else return ()                 +                  +          +   when (verbose opt >= 1) $ liftIO $ putStrLn $ "kansas-comet connect with prefix=" ++ show (prefix opt)++   -- A unique number generator, or ephemeral generator.+   -- This is the (open) secret between the client and server.+   -- (Why are we using an MVar vs a TMVar? No specific reason here)+   uniqVar <- liftIO $ newMVar 0+   let getUniq :: IO Int+       getUniq = do+              u <- takeMVar uniqVar+              putMVar uniqVar (u + 1)+              return u++   tm ::  UTCTime  <- liftIO $ getCurrentTime++   let server_id+           = Numeric.showHex (toModifiedJulianDay (utctDay tm))+           $ ("-" ++)+           $ Numeric.showHex (floor (utctDayTime tm * 1000) :: Integer)+           $ ""++   contextDB <- liftIO $ atomically $ newTVar $ (Map.empty :: Map.Map Int Document)+   let newContext :: IO Int+       newContext = do+            uq <- getUniq+            picture <- atomically $ newEmptyTMVar+            callbacks <- atomically $ newTVar $ Map.empty+            queue <- atomically $ newTChan+            let cxt = Document picture callbacks queue uq+            liftIO $ atomically $ do+                    db <- readTVar contextDB+                    -- assumes the getUniq is actually unique+                    writeTVar contextDB $ Map.insert uq cxt db+            -- Here is where we actually spawn the user code+            _ <- forkIO $ callback cxt+            return uq++   -- POST starts things off.+   post (capture $ prefix opt ++ "/") $ do+            uq  <- liftIO $ newContext+            text (LT.pack $ "$.kc.session(" ++ show server_id ++ "," ++ show uq ++ ");")++   -- GET the updates to the documents (should this be an (empty) POST?)++--   liftIO $ print $ prefix opt ++ "/act/:id/:act"+   get (capture $ prefix opt ++ "/act/" ++ server_id ++ "/:id/:act") $ do+            setHeader "Cache-Control" "max-age=0, no-cache, private, no-store, must-revalidate"+            -- do something and return a new list of commands to the client+            num <- param "id"++            when (verbose opt >= 2) $ liftIO $ putStrLn $+                "Kansas Comet: get .../act/" ++ show num+--            liftIO $ print (num :: Int)++            let tryPushAction :: TMVar T.Text -> Int -> ActionM ()+                tryPushAction var n = do+                    -- The PUSH archtecture means that we wait upto 3 seconds if there+                    -- is not javascript to push yet. This stops a busy-waiting+                    -- (or technically restricts it to once every 3 second busy)+                    ping <- liftIO $ registerDelay (3 * 1000 * 1000)+                    res <- liftIO $ atomically $ do+                            b <- readTVar ping+                            if b then return Nothing else do+                                 liftM Just (takeTMVar var)+++                    when (verbose opt >= 2) $ liftIO $ putStrLn $+                                "Kansas Comet (sending to " ++ show n ++ "):\n" ++ show res++                    case res of+                     Just js -> do+--                            liftIO $ putStrLn $ show js+                            text $ LT.fromChunks [js]+                     Nothing  ->+                            -- give the browser something to do (approx every 3 seconds)+                            text LT.empty++            db <- liftIO $ atomically $ readTVar contextDB+            case Map.lookup num db of+               Nothing  -> text (LT.pack $ "console.warn('Can not find act #" ++ show num ++ "');")+               Just doc -> tryPushAction (sending doc) num+++   post (capture $ prefix opt ++ "/reply/" ++ server_id ++ "/:id/:uq") $ do+           setHeader "Cache-Control" "max-age=0, no-cache, private, no-store, must-revalidate"+           num <- param "id"+           uq :: Int <- param "uq"+           --liftIO $ print (num :: Int, event :: String)++           when (verbose opt >= 2) $ liftIO $ putStrLn $+                "Kansas Comet: post .../reply/" ++ show num ++ "/" ++ show uq++           wrappedVal :: Value <- jsonData+           -- Unwrap the data wrapped, because 'jsonData' only supports+           -- objects or arrays, but not primitive values like numbers+           -- or booleans.+           let val = fromJust $ let (Object m) = wrappedVal+                                in HashMap.lookup (T.pack "data") m+           --liftIO $ print (val :: Value)+           db <- liftIO $ atomically $ readTVar contextDB+           case Map.lookup num db of+               Nothing  -> do+                   text (LT.pack $ "console.warn('Ignore reply for session #" ++ show num ++ "');")+               Just doc -> do+                   liftIO $ do+                         atomically $ do+                           m <- readTVar (replies doc)+                           writeTVar (replies doc) $ Map.insert uq val m+                   text $ LT.pack ""+++   post (capture $ prefix opt ++ "/event/" ++ server_id ++ "/:id") $ do+           setHeader "Cache-Control" "max-age=0, no-cache, private, no-store, must-revalidate"+           num <- param "id"++           when (verbose opt >= 2) $ liftIO $ putStrLn $+                "Kansas Comet: post .../event/" ++ show num ++           wrappedVal :: Value <- jsonData+           -- Unwrap the data wrapped, because 'jsonData' only supports+           -- objects or arrays, but not primitive values like numbers+           -- or booleans.+           let val = fromJust $ let (Object m) = wrappedVal+                                in HashMap.lookup (T.pack "data") m+           --liftIO $ print (val :: Value)++           db <- liftIO $ atomically $ readTVar contextDB+           case Map.lookup num db of+               Nothing  -> do+                   text (LT.pack $ "console.warn('Ignore reply for session #" ++ show num ++ "');")+               Just doc -> do+                   liftIO $ atomically $ do+                           writeTChan (eventQueue doc) val+                   text $ LT.pack ""+           +   return ()++-- | 'kCometPlugin' provides the location of the Kansas Comet jQuery plugin.+kCometPlugin :: IO String+kCometPlugin = do+        dataDir <- getDataDir+        return $ dataDir ++ "/static/js/kansas-comet.js"++-- | 'send' sends a javascript fragement to a document.+-- The Text argument will be evaluated before sending (in case there is an error,+-- or some costly evaluation needs done first).+-- 'send' suspends the thread if the last javascript has not been *dispatched*+-- the the browser.+send :: Document -> T.Text -> IO ()+send doc js = atomically $ putTMVar (sending doc) $! js++-- | wait for a virtual-to-this-document's port numbers' reply.+getReply :: Document -> Int -> IO Value+getReply doc num = do+        atomically $ do+           db <- readTVar (replies doc)+           case Map.lookup num db of+              Nothing -> retry+              Just r -> do+                      writeTVar (replies doc) $ Map.delete num db+                      return r++-- | 'Document' is the Handle into a specific interaction with a web page.+data Document = Document+        { sending    :: TMVar T.Text             -- ^ Code to be sent to the browser+                                                 -- This is a TMVar to stop the generation+                                                 -- getting ahead of the rendering engine+        , replies    :: TVar (Map.Map Int Value) -- ^ This is numbered replies, to ports+        , eventQueue :: TChan Value              -- ^ Events being sent+        , _secret    :: Int                      -- ^ the (session) number of this document+        }++-- 'Options' for Comet.+data Options = Options+        { prefix  :: String             -- ^ what is the prefix at at start of the URL (for example \"ajax\")+        , verbose :: Int                -- ^ 0 == none (default), 1 == inits, 2 == cmds done, 3 == complete log+        }++instance Default Options where+  def = Options+        { prefix = ""                   -- default to root, this assumes single page, etc.+        , verbose = 0+        }+++-- Defaults for 'Options'. Or you can use the defaults package.+defaultOptions :: Options+defaultOptions = def++------------------------------------------------------------------------------------++-- | Generate a @Document@ that prints what it would send to the server.+debugDocument :: IO Document+debugDocument = do+  picture <- atomically $ newEmptyTMVar+  callbacks <- atomically $ newTVar $ Map.empty+  _ <- forkIO $ forever $ do+          res <- atomically $ takeTMVar $ picture+          putStrLn $ "Sending: " ++ show res+  q <- atomically $ newTChan+  return $ Document picture callbacks q 0++-- | Fake a specific reply on a virtual @Document@ port.+debugReplyDocument :: Document -> Int -> Value -> IO ()+debugReplyDocument doc uq val = atomically $ do+   m <- readTVar (replies doc)+   writeTVar (replies doc) $ Map.insert uq val m+
kansas-comet.cabal view
@@ -1,15 +1,15 @@ Name:                kansas-comet-Version:             0.2+Version:             0.3.0 Synopsis:            A JavaScript push mechanism based on the comet idiom Homepage:            https://github.com/ku-fpg/kansas-comet/-Bug-reports:         https://github.com/ku-fpg/kansas-comet/+Bug-reports:         https://github.com/ku-fpg/kansas-comet/issues License:             BSD3 License-file:        LICENSE Author:              Andrew Gill <andygill@ku.edu>, Andrew Farmer <anfarmer@ku.edu> Maintainer:          Andrew Gill <andygill@ku.edu>-Copyright:           (c) 2013 The University of Kansas+Copyright:           (c) 2014 The University of Kansas Category:            Web-Stability:           experimental+Stability:           beta Build-type:          Simple Cabal-version:       >= 1.10 Description:@@ -19,21 +19,19 @@     static/js/kansas-comet.js  Library-  Exposed-modules:     Web.KansasComet+  Exposed-modules:     Web.Scotty.Comet   other-modules:       Paths_kansas_comet   default-language:    Haskell2010-  build-depends:       base             >= 4.5          && < 5,-                       unordered-containers >= 0.2.3    && < 0.3,-                       aeson            == 0.6.*,+  build-depends:       base             >= 4.6          && < 4.8,+                       unordered-containers >= 0.2.3    && <= 0.2.4.0,+                       aeson            == 0.7.*,                        containers       == 0.5.*,                        data-default     == 0.5.*,-                       scotty           >= 0.4.3        && < 0.5,-                       stm              >= 2.2          && < 3.0,-                       transformers     == 0.3.*,-                       text             == 0.11.*,+                       scotty           == 0.8.*,+                       stm              >= 2.2		&& < 2.5,+                       transformers     >= 0.3          && < 0.5,+                       text             >= 0.11.3.1     && < 1.2,                        time             == 1.4.*---- text is needed just for scotty's literal    GHC-options: -Wall -fno-warn-orphans 
static/js/kansas-comet.js view
@@ -3,7 +3,7 @@    var the_prefix = "";    var kansascomet_session;    var kansascomet_server;-   var eventQueues = {};   // TODO: add the use of the queue+   var eventQueues = {};    var eventCallbacks = {};    var please_debug = false; @@ -57,6 +57,7 @@              });                // TODO: Add failure; could happen         },+   // TODO: move register, send & waitFor into different library    // This says someone is listening on a specific event    // The full event name is "scope/eventname", for example    // "body/click"@@ -134,7 +135,22 @@                     data: "{ \"data\": " + $.toJSON(obj) + " }",                     contentType: "application/json; charset=utf-8",                     dataType: "json"});-   }+   },+   event: function (obj) {+      debug('event(' + $.toJSON(obj) + ')');+           $.ajax({ url: the_prefix + "/event/" + kansascomet_server + "/" + kansascomet_session,+                    type: "POST",+                    // This wrapper is needed because the JSON parser+                    // used on the Haskell side only supports objects+                    // and arrays. But the returned data might be just+                    // a number or a boolean. So this wrapper keeps +                    // the value safe to parse and has to be unwrapped +                    // on server side. Formatting it as string is also+                    // important for some reason.+                    data: "{ \"data\": " + $.toJSON(obj) + " }",+                    contentType: "application/json; charset=utf-8",+                    dataType: "json"});+   }          }; })(jQuery);