packages feed

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

{-# LANGUAGE TypeFamilies,
             ScopedTypeVariables,
             FlexibleInstances,
             MultiParamTypeClasses,
             FlexibleContexts,
             DefaultSignatures,
             TypeOperators,
             ConstraintKinds #-}
module Haste.App.Remote where
import Haste.Serialize
import Haste.JSON
import qualified Haste.JSString as S
import Haste.Concurrent
import Data.Proxy

import GHC.StaticPtr
import Haste.App.Client
import Haste.App.Protocol
import Haste.App.Routing as Routing
import Haste.App.Sandbox
import Haste.App.Transport

newtype Import dom = Import (Routing.Env (Affinity dom) -> [JSON] -> CIO JSON)

-- | The affinity, i.e. node type, of a function.
type family Affinity a where
  Affinity (a -> b) = Affinity b
  Affinity (m a)    = m

-- | The result type of a function on some node.
type family Res a where
  Res (a -> b) = Res b
  Res (m a)    = a

-- | The Haskell equivalent type of a domain-specific function.
type family HaskF c a where
  HaskF c (a -> b) = (a -> HaskF c b)
  HaskF c (m a)    = c (Hask m a)

-- | Any types @m@, @cli@ and @dom@ such that @cli@ is a function in the
--   client monad, @m@ is the type of the server node, and @dom@ is a server
--   computation that is type-compatible with @cli@.
type Dispatch dom cli =
  ( Node (Affinity dom)
  , Allowed (Affinity dom) (Affinity cli)
  , Remotable cli
  , HaskF (Affinity cli) dom ~ cli
  )

-- | Any function type @dom@ which can be exported from a node to another.
type Export dom =
  ( Remote (Affinity dom) dom
  , Serialize (Hask (Affinity dom) (Res dom))
  )

-- | A client-side frontend for a 'Remote' function.
class Remotable a where
  -- | Plumbing for turning a 'StaticKey' into a remote function, callable on
  --   the client.
  dispatch' :: Endpoint -> StaticKey -> [JSON] -> a

instance {-# OVERLAPPING #-} (Serialize a, Remotable b) => Remotable (a -> b) where
  dispatch' ep k xs x = dispatch' ep k (toJSON x : xs)

instance (MonadClient cli, Serialize a) => Remotable (cli a) where
  dispatch' ep k xs = do
    Right x <- fromJSON <$> call ep k (reverse xs)
    return x

-- | A function that may act as a server-side callback. That is, one where all
--   arguments and return values are serializable.
class (Affinity dom ~ m) => Remote m dom where
  -- | Serializify a function so it may be called remotely.
  blob :: dom -> [JSON] -> Routing.Env m -> CIO (Hask m (Res dom))

instance (Serialize a, Remote m b) => Remote m (a -> b) where
  blob f (x:xs) env =
    case fromJSON x of
      Right x' -> blob (f x') xs env
      _        -> error $ "JSON decoding failed: " ++ show x
  blob _ _ _ = error "impossible: too few arguments"

instance (Affinity (m a) ~ m, Mapping m a, Res (m a) ~ a) => Remote m (m a) where
  blob m _ env = invoke env m

call :: MonadClient from => Endpoint -> StaticKey -> [JSON] -> from JSON
call ep k xs = do
  n <- getNonce
  remoteCall ep (encodeJSON $ toJSON $ (ServerCall n k xs)) n

-- | Serializify any function of type @a -> ... -> b@ into a corresponding
--   function of type @[JSON] -> Server JSON@, with the same semantics.
--   This allows the function to be called remotely via a static pointer.
remote :: forall dom. Export dom => dom -> Import dom
remote f = Import $ \env xs ->
  toJSON <$> (blob f xs env :: CIO (Hask (Affinity dom) (Res dom)))

-- | Turn a static pointer to a serializified function into a client-side
--   function which, when fully applied, is executed on the server.
--
--   The full, somewhat unwieldy, incantation to import a server-side
--   function @f@ to the client reads:
--
-- > dispatch $ static (remote f)
--
--   Of course, the dispatch may be used as part of the invocation, rather than
--   definition, of the import instead:
--
-- > f' = static (remote f)
-- > main = runApp $ dispatch f' x0 x1 ...
dispatch :: forall cli dom. Dispatch dom cli => StaticPtr (Import dom) -> cli
dispatch f = dispatch' ep (staticKey f) []
  where ep = endpoint (Proxy :: Proxy (Affinity dom))

-- | Like 'dispatch', but makes a direct call to the server, and overrides
--   the its default endpoint. The server node must be directly attached to
--   the client making the call.
dispatchTo :: Dispatch dom cli => Endpoint -> StaticPtr (Import dom) -> cli
dispatchTo ep f = dispatch' ep (staticKey f) []