mbr/elm-wss/2.1.0

NOTE: the latest version is 3.0.0

WebsocketSimple.Connection

A managed WebSocket connection.

The connection manager keeps a WebSocket connected and reconnects when it closes or reports a connection failure. By default, it retries immediately, then backs off exponentially from 250 milliseconds to a maximum delay of one minute. Retries are unlimited, and establishing a connection resets the backoff.

Each attempt has its own handle by default; see Delayed replies below. The manager handles connection lifecycle and retries so your application can focus on its protocol.

Using the application-owned ports described in WebsocketSimple, this example replies to server-sent text with its reverse and counts palindromic inputs:

import WebsocketPorts as Ports
import WebsocketSimple.Connection as Connection


type alias Model =
    { connection : Connection.Model
    , palindromes : Int
    }


type Msg
    = ConnectionMsg Connection.Msg
    | OtherMessage


init : () -> ( Model, Cmd Msg )
init _ =
    let
        ( connection, action ) =
            Connection.init (Connection.config "wss://example.com/messages")
    in
    ( { connection = connection, palindromes = 0 }
    , Connection.perform Ports.wsCmd ConnectionMsg action
    )


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ConnectionMsg connectionMsg ->
            let
                ( connection, action, incoming ) =
                    Connection.update connectionMsg model.connection


                updated =
                    { model | connection = connection }


                ( next, command ) =
                    case incoming of
                        Just event ->
                            handleEvent event updated


                        Nothing ->
                            ( updated, Cmd.none )
            in
            ( next
            , Cmd.batch [ Connection.perform Ports.wsCmd ConnectionMsg action, command ]
            )


        OtherMessage ->
            ( model, Cmd.none )


handleEvent : Connection.Event -> Model -> ( Model, Cmd Msg )
handleEvent event model =
    case event of
        Connection.Text body ->
            let
                reversed =
                    String.reverse body


                next =
                    if body == reversed then
                        { model | palindromes = model.palindromes + 1 }


                    else
                        model
            in
            ( next
            , Connection.send Ports.wsCmd reversed model.connection
            )


        _ ->
            ( model, Cmd.none )


subscriptions : Model -> Sub Msg
subscriptions _ =
    Connection.subscribe Ports.wsMsg |> Sub.map ConnectionMsg

Receiving "ABC" sends back "CBA". Receiving "ABA" sends back "ABA" and increments palindromes.

Delayed replies

By default, the manager generates a fresh handle for each connection attempt. A reply command created while handling a received message in update stays bound to that connection: reconnecting cannot redirect it to a replacement socket. This is not a delivery guarantee; the socket may close before the command executes.

If work continues asynchronously after update returns, and its reply must not cross connections, use one of these approaches:

  • Capture the handle from get when starting the work and compare it with get when the result arrives. Discard the result if the handles no longer match.
  • Handle Established by resetting connection-specific protocol state and invalidating queued work or outstanding request IDs from the previous connection.

These approaches are useful when your protocol has connection-specific state, such as an initial snapshot followed by updates. The manager does not reset application state itself; your application decides what to retain across reconnects.

If replies need not stay on their original connection, simply send using the current model. withHandleReuse True additionally lets already-created commands address a replacement socket by reusing the same handle across attempts. This only works if the replacement is open when the command executes. Handle comparisons can no longer distinguish attempts, though Established is still emitted for each successful connection. Calls to send and sendBinary while disconnected are still discarded; there is no buffering or replay.

Caller-initiated dis- and reconnects

Call fail when your application determines that the current connection is unusable: Connection.fail "Update sequence gap" model.connection. This records an ApplicationError, closes the socket, and follows the configured retry schedule and attempt limit. It only affects an established connection; calls while connecting, retrying, waiting, or exhausted do nothing. Transport failures during connection attempts still trigger the normal retry behavior.

Use reconnect for a "Reconnect now" button. It replaces an active connection or skips a pending backoff, starting a fresh attempt immediately. It also works after exhaustion, resetting the retry count and backoff while retaining your configuration. Old retry callbacks cannot restart the connection again. Like automatic retries, it honors withHandleReuse and leaves application protocol state to you.

Both functions return a model and an action to pass to perform:

reconnectNow : Model -> ( Model, Cmd Msg )
reconnectNow model =
    let
        ( connection, action ) =
            Connection.reconnect model.connection
    in
    ( { model | connection = connection }
    , Connection.perform Ports.wsCmd ConnectionMsg action
    )

Connection liveness

A server can check client liveness by sending WebSocket protocol pings and waiting for pongs. Browsers reply automatically. JavaScript cannot send or observe these control frames, so they do not count as activity for client-side checks.

A client can check server liveness by timing out a silent server. The server sends unsolicited heartbeat data frames, either periodically or only when otherwise idle. An empty binary frame works well: the application can discard Binary events whose payload has Bytes.width zero. The JSON adapter passes binary frames through too, so these heartbeats need no JSON representation.

Enable the client-side timeout with Connection.withServerTimeout 45000. It starts when the connection opens; every incoming text or binary frame counts as activity. If the server stays silent for the configured milliseconds, the manager records ServerTimeout, closes the socket, and follows its retry policy. Checks are disabled by default and do not time out connection attempts. Choose a timeout longer than the heartbeat interval, with room for network and scheduling delays.

Configuration

type Config

Connection settings, applied before the first attempt.

config : String -> Config

Configure an indefinitely maintained connection with capped exponential backoff.

withSchedule : Schedule -> Config -> Config

Choose the delay between retries.

withLimit : AttemptLimit -> Config -> Config

Limit attempts in a retry sequence, including its initial attempt.

Give the manager its own handle namespace. Use distinct namespaces when maintaining multiple connections; socket handles are generated within it.

Reuse one handle across attempts when enabled. The default is False. Delayed sends can then address a replacement socket, and handles no longer identify individual attempts. Retry messages still belong to distinct attempts.

withProtocols : List String -> Config -> Config

Request an ordered list of WebSocket subprotocols.

Reconnect after this many milliseconds without incoming data on an established connection. Disabled by default. Nonpositive or non-finite values disable checks.

State and transitions

type Model

A retrying connection whose successful value is its connected socket handle.

type Msg

Socket input, clock readings, or a scheduled retry. Route these messages back to update.

type Event
= Established (Maybe String)
| Text String
| Binary Bytes
| Error TransportErrorDetails

Application events from the managed connection. Established includes the negotiated subprotocol and lets your application initialize each new connection. Error reports errors that do not trigger reconnection; connection loss and retry failures are available through state instead.

type Action
= None
| Open Handle String (List String)
| Close Handle (Maybe ( Float, Msg ))
| Replace Handle Handle String (List String)
| ReadTime (Posix -> Msg)
| CheckAt Posix (Posix -> Msg)

One connection transition's effect, interpreted by perform. Close carries an optional retry delay in milliseconds and the message to deliver afterwards. Replace swaps the old handle's socket for a new connection without a retry delay. ReadTime timestamps activity; CheckAt waits until a deadline and supplies the actual wakeup time. Their callbacks return messages for update.

type Failure
= Closed CloseDetails
| TransportError TransportErrorDetails
| ApplicationError String
| ServerTimeout

The reason a connection was lost or an attempt failed.

Begin the first connection attempt.

update : Msg -> Model -> ( Model, Action, Maybe Event )

Process a socket event or retry, returning an application event when appropriate. Events with unrelated handles and obsolete retry messages are ignored.

Socket closure, construction failure, and browser transport failure schedule reconnection without emitting an application event. Other errors are reported as Error without reconnecting.

fail : String -> Model -> ( Model, Action )

Fail an established connection, recording an ApplicationError and closing the socket. Retries follow the configured schedule and attempt limit. Calls in any other state do nothing; use reconnect to replace a pending attempt.

Start a fresh attempt immediately, replacing any active connection or skipping its backoff. Resets the retry count, even after exhaustion, and invalidates pending retry callbacks. Configuration, including handle reuse, is preserved.

Inspect connection progress and the latest failure through Again.Remote.

get : Model -> Maybe Handle

Get the connected socket handle, or Nothing while disconnected.

Integration

Forward an event from an existing handled WebSocket subscription.

subscribe : EventPort ( Handle, RawMsg ) -> Sub Msg

Subscribe to socket events through an application-owned event port.

perform : CommandPort msg -> (Msg -> msg) -> Action -> Cmd msg

Execute one connection action through your application. A reused handle is replaced with a single open command, which closes the old socket in the runtime.

send : CommandPort msg -> String -> Model -> Cmd msg

Send text through the connected socket. Disconnected sends are discarded, not buffered or replayed. Delivery is best-effort.

sendBinary : CommandPort msg -> Bytes -> Model -> Cmd msg

Send binary data with the same best-effort behavior as send.