dingo-core 0.0.3.5 → 0.1.0
raw patch · 6 files changed
+80/−118 lines, 6 filesdep +conduitdep +wai-eventsourcedep −enumeratordep ~aesondep ~attoparsecdep ~waiPVP ok
version bump matches the API change (PVP)
Dependencies added: conduit, wai-eventsource
Dependencies removed: enumerator
Dependency ranges changed: aeson, attoparsec, wai, wai-extra, warp
API changes (from Hackage documentation)
Files
- bundles/_bootstrap/bootstrap.js +28/−44
- dingo-core.cabal +8/−8
- src/Dingo/Internal/JavaScript.hs +2/−3
- src/Dingo/Internal/Queue.hs +0/−24
- src/Dingo/Internal/Server/State.hs +7/−16
- src/Dingo/Internal/Server/Wai.hs +35/−23
bundles/_bootstrap/bootstrap.js view
@@ -2,17 +2,21 @@ * Bootstrapping code. */ var Dingo;-Dingo = (function () {- var encoders = {};- var decoders = {};+Dingo = (function ($) {+ "use strict";+ var exports, encoders, decoders; + exports = {};+ encoders = {};+ decoders = {};+ function getWidget(i) { return $('#i' + i); } function encodeAll(encoders) { var encodedState = {};- $.each(encoders, function(widgetId, encoder) {+ $.each(encoders, function (widgetId, encoder) { var widget; widget = getWidget(widgetId); if (widget.length > 0) {@@ -24,56 +28,39 @@ return encodedState; } - function addEncoder(id, encoderFunc) {+ exports.addEncoder = function (id, encoderFunc) { if (encoderFunc !== null) { encoders[id] = encoderFunc; }- }+ }; - function addDecoder(id, decoderFunc) {+ exports.addDecoder = function (id, decoderFunc) { if (decoderFunc !== null) { decoders[id] = decoderFunc; }- }+ }; - function setWidgetValue(id, json) {+ exports.setWidgetValue = function (id, json) { var d = decoders[id]; if (typeof d === 'function') { d.call(getWidget(id), json); }- }+ }; - // Long polling handler.+ // Initialize event source for receiving updates. function poll() {- var delay = 0;- // Otherwise we start one...- console.log("Starting long polling...");- $.ajax({- error : function (xhr, status, err) {- if (err === 'timeout') {- delay = 0; // timeout means we need to retry immediately- } else {- delay = 15000; // don't hammer the server- }- },- success : function (data, status, xhr) {- // Start polling immediately again.- delay = 0;- },- complete : function () {- // Restart poll.- setTimeout(poll, delay);- },- timeout : 60000,- data : { },- type : 'GET',- dataType : 'script',- url: '/poll'- });+ var eventSource;++ console.log("Starting event source...");++ eventSource = new EventSource("poll");+ eventSource.onmessage = function(msg) {+ eval(msg.data);+ }; } // Set up the callback handler.- function callback(id) {+ exports.callback = function (id) { var encodedState; // Go. encodedState = encodeAll(encoders);@@ -98,14 +85,11 @@ dataType : 'script', url: '/callback/' + id });- }+ };+ // Return the methods.- return { 'callback' : callback,- 'addEncoder' : addEncoder,- 'addDecoder' : addDecoder,- 'setWidgetValue' : setWidgetValue- };-}());+ return exports;+}(jQuery)); /* * Bootstrap.
dingo-core.cabal view
@@ -1,5 +1,5 @@ Name: dingo-core-Version: 0.0.3.5+Version: 0.1.0 Synopsis: Dingo is a Rich Internet Application platform based on the Warp web server. Description: Dingo is a Rich Internet Application platform based on the Warp web server. It allows you to write code which looks very much like regular GUI code and have it work in the browser. @@ -17,17 +17,17 @@ Library Build-Depends: base == 4.*- , aeson >= 0.3.2.12 && <0.4- , attoparsec >= 0.9.0.0 && < 0.10+ , aeson >= 0.5 && <0.6+ , attoparsec >= 0.10 && < 0.11 , base64-bytestring >= 0.1 && < 0.2 , bytestring >= 0.9.0.1 , blaze-builder >= 0.3 && <0.4 , blaze-html >= 0.4.1.6 && < 0.5 , blaze-textual >= 0.2.0.5 && < 0.3+ , conduit >= 0.1 && < 0.2 , containers >= 0.4 , cookie >= 0.3.0 && < 0.4 , deepseq >= 1.1 && < 1.2- , enumerator >= 0.4 && < 0.5 , file-embed >= 0.0.4 && < 0.1 , fclabels == 1.0.* , hashable >= 1.1.0.0 && < 1.2@@ -40,9 +40,10 @@ , text == 0.11.* , transformers >= 0.2.2 && < 0.3 , unordered-containers >= 0.1.4 && < 0.2- , wai == 0.4.*- , wai-extra == 0.4.*- , warp == 0.4.*+ , wai == 1.0.*+ , wai-eventsource == 1.0.*+ , wai-extra == 1.0.*+ , warp == 1.0.* , web-css == 0.1.* Extensions: CPP DeriveDataTypeable@@ -72,7 +73,6 @@ Dingo.Internal.EventTypes Dingo.Internal.Html Dingo.Internal.JavaScript- Dingo.Internal.Queue Dingo.Internal.ResourceBundle.Internal Dingo.Internal.ResourceBundle.Boot Dingo.Internal.Server.State
src/Dingo/Internal/JavaScript.hs view
@@ -2,7 +2,6 @@ ( renderCommandsToJs ) where -import Blaze.ByteString.Builder (toLazyByteString) import Data.Aeson (Value) import Data.Aeson.Encode (fromValue) import Data.ByteString (ByteString)@@ -10,7 +9,7 @@ import Data.Monoid (mconcat) import Data.Text (Text) import qualified Data.Text as TS-import Data.Text.Lazy.Encoding (encodeUtf8, decodeUtf8)+import Data.Text.Lazy.Encoding (encodeUtf8) import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Builder as TLB import Dingo.Internal.Base (Command(..))@@ -22,7 +21,7 @@ -- Convert a JSON value to Javascript. instance ToJavascript Value where toJavascript json =- toJavascript $ decodeUtf8 $ toLazyByteString $ fromValue json+ toJavascript $ TLB.toLazyText $ fromValue json jsHtml :: Html -> TL.Text jsHtml html = TLB.toLazyText $
− src/Dingo/Internal/Queue.hs
@@ -1,24 +0,0 @@-module Dingo.Internal.Queue- ( readChanTimeout- ) where--import Control.Concurrent (forkIO, threadDelay, killThread, myThreadId)-import Control.Concurrent.Chan.Strict (Chan, readChan)-import Control.Concurrent.MVar.Strict (newEmptyMVar, putMVar, takeMVar)-import Control.DeepSeq (NFData)---- Read a Chan with timeout.-readChanTimeout :: NFData a => Chan a -> Int -> IO (Maybe a)-readChanTimeout ch timeout = do- result <- newEmptyMVar- _ <- forkIO $ do- wid <- myThreadId- readerThreadId <- forkIO $ do- x <- readChan ch- killThread wid- putMVar result (Just x)- threadDelay timeout- killThread readerThreadId- putMVar result Nothing- -- Block waiting for either timeout or the read.- takeMVar result
src/Dingo/Internal/Server/State.hs view
@@ -8,12 +8,12 @@ , getSession , getSessionId , handleCallback- , readCommandWithTimeout+ , readCommand , ssApplicationTitle , ssBootResourceBundles ) where -import Control.Concurrent.Chan.Strict (Chan, newChan, writeChan)+import Control.Concurrent.Chan.Strict (Chan, newChan, readChan, writeChan) import Control.DeepSeq.ByteString () import Control.Monad (replicateM) import Data.Aeson (Value(..))@@ -31,7 +31,6 @@ import Dingo.Internal.Base (CallbackId, WidgetId) import Dingo.Internal.CallbackTypes (WrapCallback(..), CallbackM, mkSession, getCommands) import Dingo.Internal.JavaScript (renderCommandsToJs)-import Dingo.Internal.Queue (readChanTimeout) import Dingo.Internal.ResourceBundle.Boot (bootResourceBundles) import Dingo.Internal.Session (SessionState, runSessionT, runCallback) import Dingo.ResourceBundle (ResourceBundle)@@ -130,16 +129,8 @@ -- Update the session reference. atomicModifyIORef clientSessionRef (\_ -> (ClientSessionState sessionState' commandsChan, ())) --- Wait for a command to appear in the queue.-readCommandWithTimeout :: ServerState -> ClientSession -> Int -> IO (Maybe ByteString)-readCommandWithTimeout serverState clientSession timeout = do- serverStateM <- readIORef $ L.get ssClientSessions serverState- case M.lookup clientSessionId serverStateM of- Nothing -> return Nothing- Just sessionRef -> do- commandsChan <- fmap (L.get cssCommandsForClient) $ readIORef sessionRef- readChanTimeout commandsChan timeout-- where-- clientSessionId = fst clientSession+-- Wait for a command to appear in the queue and return it.+readCommand :: ClientSession -> IO ByteString+readCommand clientSession = do+ commandsChan <- fmap (L.get cssCommandsForClient) $ readIORef $ snd clientSession+ readChan commandsChan
src/Dingo/Internal/Server/Wai.hs view
@@ -3,33 +3,37 @@ ) where import Blaze.ByteString.Builder (fromByteString, toByteString)+import Control.Concurrent (threadDelay)+import Control.Concurrent.Chan (newChan, writeChan) import Control.DeepSeq.ByteString ()-import Control.Monad (forM_)+import Control.Monad (forM_, forever, void) import Control.Monad.IO.Class (liftIO)+import Control.Monad.Trans.Resource (resourceForkIO) import Data.Aeson (Value(..)) import qualified Data.Aeson.Parser as AEP import Data.Attoparsec (Parser) import qualified Data.Attoparsec as AP import Data.ByteString (ByteString)-import Data.Enumerator (Iteratee)+import qualified Data.ByteString as B+import Data.Conduit (ResourceT, ($$)) import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as H import Data.IORef (readIORef) import qualified Data.Label as L-import qualified Data.Map as M import Data.Maybe (fromMaybe, mapMaybe) import Data.Monoid (mempty) import Data.Text (Text) import qualified Data.Text.Encoding as TE import Dingo.Internal.Base import Dingo.Internal.Html (mkHeadMerge)-import Dingo.Internal.Server.State (ServerState, ClientSession, SessionId, ssApplicationTitle, ssBootResourceBundles, handleCallback, readCommandWithTimeout, getSession, getSessionId, cssClientSessionState)+import Dingo.Internal.Server.State (ServerState, ClientSession, SessionId, ssApplicationTitle, ssBootResourceBundles, handleCallback, readCommand, getSession, getSessionId, cssClientSessionState) import Dingo.Internal.Session (lookupResource) import Dingo.ResourceBundle (ResourceBundle) import Network.HTTP.Types (statusOK, statusForbidden, headerContentType, Header, ResponseHeaders) import Network.Wai (Request(..), Response(..)) import qualified Network.Wai as W import Network.Wai.Parse (parseRequestBody, lbsSink)+import Network.Wai.EventSource (eventSourceApp, ServerEvent(..)) import Text.Blaze ((!), toHtml, unsafeByteStringValue) import qualified Text.Blaze.Html4.Strict as H4 import qualified Text.Blaze.Html4.Strict.Attributes as A@@ -40,13 +44,9 @@ headerSetCookie :: SetCookie -> Header headerSetCookie s = ("Set-Cookie", toByteString $ renderSetCookie s) --- Timeout for poll requests.-longPollTimeout :: Int-longPollTimeout = 30 * 1000 * 1000---- JavaScript content type.-jsContentType :: ByteString-jsContentType = "text/javascript;charset=utf-8"+-- Keep-alive interval for events.+eventSourceKeepAlive :: Int+eventSourceKeepAlive = 5 * 1000 * 1000 -- HTML content type. htmlContentType :: ByteString@@ -102,7 +102,7 @@ Left _ -> H.empty -- Error parsing; ignore. Right (Object o) ->- H.fromList $ mapMaybe f $ M.toList o+ H.fromList $ mapMaybe f $ H.toList o Right _ -> H.empty -- Invalid format; ignore. where@@ -110,15 +110,15 @@ fmap (\i -> (i,v)) $ parseOnlyText widgetIdParser k -- Callback server part.-callback :: Request -> ClientSession -> Text -> Iteratee ByteString IO Response+callback :: Request -> ClientSession -> Text -> ResourceT IO Response callback request clientSession callbackIdStr = handle $ parseOnlyText callbackIdParser callbackIdStr where- handle :: Maybe CallbackId -> Iteratee ByteString IO Response+ handle :: Maybe CallbackId -> ResourceT IO Response handle Nothing = return forbiddenResponse handle (Just callbackId) = do -- Post body contains URL-encoded data.- (params, _) <- parseRequestBody lbsSink request+ (params, _) <- requestBody request $$ parseRequestBody lbsSink request let stateJson = fromMaybe "{}" $ lookup "state" params -- Get the state update portion of the form data. let stateUpdates = convertStateUpdates stateJson@@ -127,11 +127,24 @@ return emptyOKResponse -- Long polling handler.-poll :: ServerState -> ClientSession -> Iteratee ByteString IO Response-poll serverState clientSession = do- command <- liftIO $ readCommandWithTimeout serverState clientSession longPollTimeout- let command_ = fromMaybe "" command- return $ ResponseBuilder statusOK [headerContentType jsContentType] $ fromByteString command_+poll :: ClientSession -> Request -> ResourceT IO Response+poll clientSession request = do+ evChan <- liftIO $ newChan+ -- Introduce a thread which issues keep-alive events.+ void $ resourceForkIO $ forever $ liftIO $ do+ threadDelay eventSourceKeepAlive+ writeChan evChan $ CommentEvent $ fromByteString "keep-alive"+ -- Introduce a thread which transforms commands from client session to events.+ void $ resourceForkIO $ forever $ liftIO $ do+ c <- readCommand clientSession+ writeChan evChan $ ServerEvent Nothing Nothing+ [ fromByteString $ B.map escape c ]+ -- Serve the events.+ eventSourceApp evChan request+ where+ -- Replace all non-printable characters with a space.+ escape c | c < 0x20 = 0x20+ escape c | otherwise = c -- Generate SetCookie session header if necessary. generateSessionSetCookie :: Maybe SessionId -> ClientSession -> Maybe SetCookie@@ -140,7 +153,7 @@ | otherwise = Just $ mkSessionCookie $ getSessionId clientSession -- Serve a bundle.-serveBundle :: ClientSession -> [Text] -> Iteratee ByteString IO Response+serveBundle :: ClientSession -> [Text] -> ResourceT IO Response serveBundle _ [] = return forbiddenResponse serveBundle clientSession (bundleId:path) = do clientSessionStateRef <- liftIO $ readIORef (snd clientSession)@@ -155,7 +168,6 @@ mkWaiApplication :: ServerState -> W.Application mkWaiApplication serverState = loop where- loop :: Request -> Iteratee ByteString IO Response loop request = do -- Extract the cookies. let cookies =@@ -178,7 +190,7 @@ -- Process the request case pathInfo request of [] -> return $ indexResponse serverState responseHeaders- ["poll"] -> poll serverState clientSession+ ["poll"] -> poll clientSession request ["callback",cid] -> callback request clientSession cid ("bundles":bundlePath) -> serveBundle clientSession bundlePath _ -> return forbiddenResponse