packages feed

haste-app-0.1.0.0: src/Haste/App/Routing.hs

{-# LANGUAGE UndecidableInstances,
             ScopedTypeVariables,
             TypeFamilies,
             FlexibleInstances,
             MultiParamTypeClasses,
             FlexibleContexts,
             GeneralizedNewtypeDeriving,
             CPP,
             DefaultSignatures #-}
-- | Type-level routing of requests from clients to servers attached to a
--   network and back again.
module Haste.App.Routing
  ( Node (..), NodeEnv (..), MonadReader (..), Mapping (..)
  , Server, EnvServer, invokeServer, localEndpoint
  ) where
import Control.Monad.Reader
import Data.Default
import Data.Proxy
import Haste.Serialize -- for serialization
import Haste.JSON
import Haste.App.Protocol
import Haste.Concurrent (MonadConc (..), CIO)
import Haste (JSString, fromJSStr)
import Haste.App.Client.Type (Client) -- for default Parent type instance
import Haste.App.Server.Type
import Haste.App.Transport (MonadClient)
import GHC.StaticPtr
import GHC.Exts (Constraint)

-- for default Endpoint instance
import Data.Typeable
import Data.Char
import Data.Word (Word16)
import Haste (getLocationHostName)
import System.IO.Unsafe
import System.IO (hPutStrLn, stderr)

-- * Defining and calling servers

-- | A server node in the network.
--   To define a new node @N@, the following must be provided:
--
--     * The type of the node's environment. This may be omitted if the node
--       has no environment, in which case it defaults to @()@.
--
--     * A function @endpoint@, which describes how to physically reach
--       the node; this may be omitted if the node is reachable via WebSockets
--       on the same host name as the client program is served from.
--       In this case, the port of the endpoint is determined by the hash of
--       the node's @TypeRep@.
--
--     * A function @init@, which initializes the node's environment and
--       performs any server-side initialization. This may be omitted if
--       @Env m@ is an instance of 'Default' and no other server-side setup
--       is needed.
--
--     * At least one instance of 'Mapping'. This can be omitted if @N@ is
--       a case of 'EnvServer'.
--
--   Thus, the minimal declaration to create a new node with
--   some environment @MyEnv@ would be:
--
-- > type MyNode = EnvNode MyEnv
-- > instance Node MyNode where
-- >   type Env MyNode = MyEnv
--
--   If a stateful node is desired rather than one with an environment, this
--   can be accomplishe using, for instance, @IORef@s:
--
-- > type MyNode = EnvNode (IORef MySt)
-- > instance Node MyNode where
-- >   type Env MyNode = IORef MySt
-- >   init _ = liftIO $ newIORef initialState
--
--   A stateful node which is not a case of @EnvServer@ requires slightly more
--   boilerplate:
--
-- > newtype MyNode a = MyNode (EnvServer (IORef MySt) a)
-- >   deriving (Functor, Applicative, Monad, MonadIO, MonadReader (IORef MySt))
-- > 
-- > instance Mapping MyNode a where
-- >   invoke env (MyNode m) = invokeServer env m
-- > 
-- > instance Node MyNode where
-- >   type Env MyNode = IORef MySt
-- >   init _ = liftIO $ newIORef initialState
class Node (m :: * -> *) where
  -- | Environment type of node. Defaults to @()@.
  type Env m :: *
  type Env m = ()

  -- | The type class describing all clients that may make calls to this node.
  --   By default, any 'MonadClient' may call a node.
  type Allowed m (c :: * -> *) :: Constraint
  type Allowed m c = ()

  -- | The location at which the node can be reached.
  endpoint :: Proxy m -> Endpoint
  default endpoint :: Typeable m => Proxy m -> Endpoint
  endpoint p = unsafePerformIO $ do
    let node = show $ typeRep p
        w16 = djb2 node
        port = fromIntegral w16 `rem` (65535-1024) + 1024
#ifdef __HASTE__
    host <- fromJSStr <$> getLocationHostName
#else
    hPutStrLn stderr $ "selecting port " ++ show port ++ " for node " ++ node
    let host = ""
#endif
    return $ WebSocket (if "" == host then "localhost" else host) port
    where
      -- | DJB2 hash function
      djb2 :: String -> Word16
      djb2 = go 5381
        where
          go n (c:s) = go (n*33 + (fromIntegral $ ord c)) s
          go n _     = n

  -- | Initialization for the given node.
  init :: Proxy m -> CIO (Env m)
  default init :: Default (Env m) => Proxy m -> CIO (Env m)
  init _ = return def

-- | A mapping from node return values to Haskell values.
--   This is useful when making nodes out of e.g. DSLs where the DSL-internal
--   type is not what the Haskell host program gets back from running it.
--   One instance of this is @opaleye@, another is @aplite@.
--
--   Most nodes will only need a single instance:
--
-- > instance Mapping MyNode a where
-- >   invoke env node = invokeMyNode env node
--
--   This instance is already provided for all nodes of type 'EnvServer'.
class Mapping (m :: * -> *) a where
  type Hask m a
  type Hask m a = a

  -- | Run a DSL computation which returns an @a@ on the DSL level,
  --   corresponding to @Map m a@ on the Haskell level.
  invoke :: Env m -> m a -> CIO (Hask m a)
  default invoke :: (m ~ EnvServer (Env m), Hask m a ~ a) => Env m -> m a -> CIO a
  invoke = invokeServer

-- | Node environment tagged with its type, to avoid having to pass a Proxy
--   around to identify the type of the node.
newtype NodeEnv m = NodeEnv {unNE :: Env m}

instance (t ~ Env (EnvServer t)) => Mapping (EnvServer t) a