ensoft / entrance / EnTrance.Channel

EnTrance clients talk to a server over a single websocket. A "channel" is like a tunnel inside that websocket, dedicated to one part of your Elm application.

Simple apps need only a single channel. The value comes when your app becomes more complex, and gains more modular structure. Often you end up with several different "sub-apps", each of which has responsibility for some part of the overall user experience. In such cases, it's easiest to create one channel per sub-app, so that each sub-app has its own two-way communciation mechanism with the features it invokes on the server, without coupling it to the other sub-apps.

You should never need more than one channel in a given module. If you're thinking about that, you probably want multiple RpcData values in your model instead.

How to use

Looking at some of the sample apps, eg the notes app may be a helpful supplement to the description here.

Channels and ports

A channel is created via either two or three ports in an Elm module. The two-port case looks like this:

import EnTrance.Channel as Channel

port myAppSend : Channel.SendPort msg

port myAppRecv : Channel.RecvPort msg

This creates a send/receive port pair, implementing one channel, called "myApp". You can choose almost anything you like for the "myApp" bit.

This is a bit weird and magical, based on the names of the ports:

So you can choose any channel name you like ("myApp" in this case), but:

The three-port case is where you also specify a third port:

port myAppIsUp : Channel.IsUpPort msg

This gives you up/down notifications for the channel state, ie whether there is a server on the end of it. If you have a Msg constructor like this:

type Msg = ...
         | AppIsUp Bool

and instantiate an IsUpPort:

port myAppIsUp : Channel.IsUpPort msg

then you can just subscribe to myAppIsUp AppIsUp and you'll receive an AppIsUp message whenever the connection state changes.


type alias SendPort msg =
Json.Encode.Value -> Platform.Cmd.Cmd msg

A port to send messages over a channel to the server.


type alias RecvPort msg =
(Json.Encode.Value -> msg) -> Platform.Sub.Sub msg

A port to receive notifications over a channel from the server.


type alias IsUpPort msg =
(Basics.Bool -> msg) -> Platform.Sub.Sub msg

A port from which to be notified of channel up/down status.

Sending messages to the server

You typically send an RPC message to the server using sendSimpleRpc, sendRpc or send. The key distinction between these three options is:

sendSimpleRpc : { model | result : EnTrance.Types.RpcData result, sendPort : SendPort msg } -> EnTrance.Request.Request -> ( { model | result : EnTrance.Types.RpcData result, sendPort : SendPort msg }, Platform.Cmd.Cmd msg )

Simplified variant of sendRpc that assumes the relevant RpcData in your model is called result, and sets the state to Loading for you.

This assumes your model includes a field called sendPort of type SendPort msg.

sendRpc : { model | sendPort : SendPort msg } -> EnTrance.Request.Request -> ( { model | sendPort : SendPort msg }, Platform.Cmd.Cmd msg )

Send an RPC request. If you're just starting out, sendSimpleRpc may be easier. If you need more control, use sendRpcCmd.

Updating the relevant RpcData to state Loading is your responsibility.

This assumes your model includes a field called sendPort of type SendPort msg.

send : { model | sendPort : SendPort msg } -> EnTrance.Request.Request -> ( { model | sendPort : SendPort msg }, Platform.Cmd.Cmd msg )

Simple way to send an async message. If you need more control, use sendCmd.

This assumes your model includes a field called sendPort of type SendPort msg.

sendRpcCmd : SendPort msg -> EnTrance.Request.Request -> Platform.Cmd.Cmd msg

Create a command to send a Request over a channel with RPC semantics (the mainline case). In many cases, using sendRpc is nicer than calling this directly.

RPC semantics mean that exactly one reply notification is expected (either success or error), and that a unique message identifier is used to ensure that out-of-order replies don't get mistaken for the reply we're waiting for here.

port appSend : SendPort cmd

Request.new "some_message"
    |> Channel.sendRpcCmd appSend

sendRpcCmds : SendPort msg -> List EnTrance.Request.Request -> Platform.Cmd.Cmd msg

Handy way to send a list of requests in one shot.

sendCmd : SendPort msg -> EnTrance.Request.Request -> Platform.Cmd.Cmd msg

Create a command to send a Request over a channel with async (fire-and-forget) semantics.

port appSend : SendPort msg

Request.new "some_message"
    |> Channel.sendCmd myAppSend

sendCmds : SendPort msg -> List EnTrance.Request.Request -> Platform.Cmd.Cmd msg

Helper to send a list of async commands in one shot.

Receiving notifications from the server

In order to receive notifications from the server (such as RPC replies), use sub to subscribe to notifications you receive on this channel. The notification arrives in JSON format, so you need to supply a set of candidate JSON decoders, that turn any expected JSON notification into a Msg for your update function.

For example, suppose you are using the built-in Syslog and Netconf features, so expect to receive one of those two notifications on the "myApp" channel. Just create Msg constructors for those two options:

type Msg
    = ...
    | GotSyslog Syslog
    | GotNetconfResult String
    | Error String

Then the 'sub' function turns the type-safe decoder for each feature (Syslog.decode and Netconf.decodeRequest) into a subscription like this:

Channel.sub myAppRecv
    Error
    [ Syslog.decode GotSyslog
    , Netconf.decodeResult GotNetconfResult
    ]

Such a subscription means that whenever you receive an incoming notification on the channel, either:

sub : RecvPort msg -> (String -> msg) -> List (Json.Decode.Decoder msg) -> Platform.Sub.Sub msg

Subscribe to a receive port, and use the specified list of decoders to turn JSON notifications for that channel into Msgs of your choice. Takes a receive port, an error message constructor, and a list of individual notification decoders.

Error and inject pseudo-channels

There are two "magic" channels, that can be instantiated only once in your application. These consume the channel names "error" and "inject".

The error pseudo-channel

You should instantiate exactly one "error" receive port in your application, and handle the (rare) errors signalled there:

port errorRecv : Channel.ErrorRecvPort

This is used to signal unexpected errors that can't be associated with a particular operation, such as the server complaining it can't decode valid JSON out of a request it received.


type alias ErrorRecvPort msg =
(String -> msg) -> Platform.Sub.Sub msg

A port from which to be notified of global errors of type String. You should have exactly one of these in your application, named errorPort.

The inject pseudo-channel

Finally, you can optionally create a pair of send/receive channels with the magic name inject, and special types:

port injectSend : Channel.InjectSendPort

port injectRecv : Channel.InjectRecvPort

These "loop round", so a JSON message send out on injectSend is received on injectRecv. This can provide a handy way for complex apps to feed messages from sub-apps back to the top level in a clean way. (You can do it lots of other ways if you prefer.) For an example of how to use it, see the example code.


type alias InjectSendPort msg =
Json.Encode.Value -> Platform.Cmd.Cmd msg

A port to inject a JSON value out of. You can have one of these in your application if you want, called injectSend.


type alias InjectRecvPort msg =
(Json.Encode.Value -> msg) -> Platform.Sub.Sub msg

A port to subcribe to, for injected JSON values. You can have one of these in your application if you want to, named injectRecv.