Elm-websocket-manager — WebSockets with reconnection and fast binary (Bytes) support

I was nerd-sniped into building a new websocket package: elm-websocket-manager.

image

Video link hosted on GitHub.

What it does: Type-safe WebSocket management via ports, with two features I couldn’t find together elsewhere:

  • Binary (Bytes) support with zero JSON overhead. An XHR monkeypatch routes ArrayBuffer data directly between Elm’s Http kernel and the WebSocket — no JSON encoding/decoding of binary payloads. A 100MB round trip through an echo server takes about 1 second. Credit to @lue-bird for the technique. (EDIT: for showing me the technique ^^)
  • Configurable reconnection with exponential backoff, jitter, max retries, and skip codes — all managed on the JS side, with Reconnecting, Reconnected, and ReconnectFailed events delivered to Elm.

Setup is minimal — declare two ports and call one JS init function:

port wsOut : WS.CommandPort msg
port wsIn : WS.EventPort msg
import * as wsm from "elm-websocket-manager";
wsm.init({ wsOut: app.ports.wsOut, wsIn: app.ports.wsIn });

API uses bind to produce a record of command functions:

chatConfig = WS.init "ws://example.com/chat"
chatWs = WS.bind chatConfig wsOut GotChatEvent

-- Then use it directly
chatWs.open
chatWs.sendText "hello"
chatWs.sendBytes myBytes
chatWs.close

Events are a single sum type — pattern match to handle everything:

case event of
    WS.Opened               -> ...
    WS.MessageReceived data -> ...
    WS.BinaryReceived bytes -> ...
    WS.Closed info          -> ...
    WS.Reconnecting info    -> ...
    WS.Reconnected          -> ...
    WS.ReconnectFailed      -> ...
    WS.Error message        -> ...
    WS.NoOp                 -> ( model, Cmd.none )

It also supports multiple simultaneous connections, RFC 6455 close codes as a union type, and a connection state helper for UI updates.

The readme includes a comparison with billstclair/elm-websocket-client and kageurufu/elm-websockets if you want to see how they differ.

The package is not yet published on the Elm package registry — for now, use it as a local source directory or git dependency.

Temporary API docs: API docs with elm-doc-preview

Feedback is welcome!

12 Likes

correction: I played no part in discovering the XHR patching trick. Full credit to Andreas Molitor (anmolitor (Andreas Molitor) · GitHub) or whoever discovered it first

2 Likes

Not elm related, but thank you for making your JS interop a single JS file. It makes my life so much easier.

5 Likes

Great stuff and I particularly appreciate the automatic reconnect, and passing up of connection fail and other error events.

I would like to express my frustration at the lack of support for Bytes over ports. It is such an obviously useful thing to be able to do, yet we have to resort to this XHR hack to achieve it. Unfortunately it seems like this hack is going from being a dirty secret of the application writer to a mainstream technique of the re-usable package author - which means it will begin to propagate into more and more places.

If we had bytes over ports, we would still have an Elm kernel that captures this use case, and therefore keeps Elm code portable should that kernel be re-implemented for some other platform. Sadly, this is not a sustainable way to extend Elms capabilities.

No criticism of this websocket package intended - what else could you have done to support bytes in a reasonable way (not base64 encoding!).


For reference here is the PR for a patch against guida to support bytes over ports, ported from the Lamdera implementation. It isn’t huge or difficult:

1 Like

Yes having support for Bytes over ports would be awesome. Lamdera has it as you mentioned.

I’m curious what are the reasons for not having it in the first place. I can imagine it breaking some immutability guarantees if the bytes are mutated once they land in JS. But I mean, that is also already the case for Value today sent through a JS → Elm port. Bytes are already not comparable anyway in Elm, so I think the tradeoff of supporting Bytes through ports is worth it IMO.

Evan if you read this, it would be lovely to have that as part of the improvements in an upcoming 0.19.2 :slight_smile:
Benchmarks here for the reasons why it matters: GitHub - lue-bird/elm-bytes-ports-benchmark: compares different ways to transport bytes over ports

1 Like

I suspect just accidental omission - the values that can be passed over ports was decided before the elm/bytes package was a thing. Possibly.

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.