----------------------------------------------------------------------------- {-# LANGUAGE TypeApplications #-} {-# LANGUAGE CPP #-} ----------------------------------------------------------------------------- -- | -- Module : Miso.WebSocket -- Copyright : (C) 2016-2026 David M. Johnson -- License : BSD3-style (see the file LICENSE) -- Maintainer : David M. Johnson <code@dmj.io> -- Stability : experimental -- Portability : non-portable -- -- = Overview -- -- "Miso.WebSocket" provides a full-duplex -- <https://developer.mozilla.org/en-US/docs/Web/API/WebSocket WebSocket> -- client that integrates directly into the MVU loop. Every operation -- — connecting, sending, and closing — returns an 'Miso.Effect.Effect', -- making WebSocket communication a first-class citizen of the @update@ -- function. -- -- = Quick start -- -- @ -- import "Miso" -- import "Miso.WebSocket" -- -- data Action -- = Connect -- | Connected 'WebSocket' -- | Received 'Miso.String.MisoString' -- | Disconnected 'Closed' -- | WsError 'Miso.String.MisoString' -- | Send 'Miso.String.MisoString' -- | Disconnect -- -- -- Hold the socket handle in the model so we can send later -- data Model = Model { ws :: 'WebSocket' } -- -- update :: Action -> 'Miso.Effect.Effect' p props Model Action -- update Connect = -- 'connectText' \"wss:\/\/echo.websocket.org\" -- Connected Disconnected Received WsError -- update (Connected sock) = -- 'Miso.State.modify' (\\m -> m { ws = sock }) -- update (Received msg) = -- 'Miso.Effect.io_' (consoleLog msg) -- update (Send txt) = do -- sock <- 'Miso.State.gets' ws -- 'sendText' sock txt -- update Disconnect = do -- sock <- 'Miso.State.gets' ws -- 'close' sock -- update _ = pure () -- @ -- -- = Connection variants -- -- Five @connect@ functions cover every wire format. They all share the -- same callback signature — @onOpen@, @onClosed@, @onMessage@, @onError@ -- — but differ in how the message payload is decoded: -- -- ['connectText'] 'Miso.String.MisoString' — plain UTF-8 text -- ['connectJSON'] @json@ ('Miso.JSON.FromJSON' json) — auto-decoded from JSON -- ['connectBLOB'] 'Blob' — raw binary Blob -- ['connectArrayBuffer'] 'ArrayBuffer' — raw binary buffer -- ['connect'] @'Payload' json@ — mixed; caller pattern-matches the payload ADT -- -- = Sending messages -- -- The 'WebSocket' handle delivered to @onOpen@ must be stored in the -- model and passed to each send call: -- -- @ -- 'sendText' :: 'WebSocket' -> 'Miso.String.MisoString' -> 'Miso.Effect.Effect' p props model action -- 'sendJSON' :: 'Miso.JSON.ToJSON' json => 'WebSocket' -> json -> 'Miso.Effect.Effect' p props model action -- 'sendBLOB' :: 'WebSocket' -> 'Blob' -> 'Miso.Effect.Effect' p props model action -- 'sendArrayBuffer' :: 'WebSocket' -> 'ArrayBuffer' -> 'Miso.Effect.Effect' p props model action -- @ -- -- = Lifecycle -- -- * Always call 'close' when the connection is no longer needed to avoid -- resource leaks. It is safe to call 'close' multiple times — subsequent -- calls are no-ops. -- * 'socketState' lets you query the current 'SocketState' -- (@CONNECTING@, @OPEN@, @CLOSING@, @CLOSED@) asynchronously. -- * 'emptyWebSocket' (@= -1@) is a null sentinel you can store in the -- model before a connection has been established. -- -- = Types -- -- * 'WebSocket' — an opaque integer file descriptor returned by @onOpen@. -- * 'URL' — alias for 'Miso.String.MisoString'. -- * 'SocketState' — four-state enum mirroring the JS @readyState@ property. -- * 'Closed' — close event data: 'closedCode', 'wasClean', 'reason'. -- * 'CloseCode' — typed close codes from -- <https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code RFC 6455>. -- * 'Payload' — @JSON value | BLOB Blob | TEXT MisoString | BUFFER ArrayBuffer@. -- * 'Blob' / 'ArrayBuffer' — re-exported from "Miso.FFI.Internal". -- -- = See also -- -- * "Miso.EventSource" — Server-Sent Events (SSE), a unidirectional alternative -- * "Miso.Fetch" — one-shot HTTP requests -- * "Miso.Effect" — 'Miso.Effect.Effect', 'Miso.Effect.io_' ---------------------------------------------------------------------------- module Miso.WebSocket ( -- *** t'WebSocket' connect , connectJSON , connectText , connectBLOB , connectArrayBuffer , sendText , sendJSON , sendBLOB , sendArrayBuffer , close , socketState -- *** Defaults , emptyWebSocket -- *** Types , WebSocket (..) , URL , SocketState (..) , CloseCode (..) , Closed (..) , Payload (..) , Blob (..) , ArrayBuffer (..) ) where ----------------------------------------------------------------------------- import Miso.Effect import Miso.JSON import Miso.Runtime import Miso.String (MisoString) import Miso.FFI (Blob(..), ArrayBuffer(..)) ----------------------------------------------------------------------------- -- | <https://developer.mozilla.org/en-US/docs/Web/API/WebSocket> -- -- Establishes a t'WebSocket' server that receives potentially multiple different t'Payload'. -- -- It's more common to use 'connectJSON' or 'connectText'. But 'connect' can be used to received multiple -- different kinds of data from a t'WebSocket' server. -- connect :: FromJSON json => URL -- ^ URL endpoint for a t'WebSocket' connection -> (WebSocket -> action) -- ^ @onOpen@ callback w/ t'WebSocket' object for successful connection. t'WebSocket' is used here to send messages. -> (Closed -> action) -- ^ @onClosed@ method that is called when a t'WebSocket' connection has closed. -> (Payload json -> action) -- ^ @onMessage@ is a callback invoked when a message has been received from the t'WebSocket' server. -> (MisoString -> action) -- ^ Error message callback -> Effect parent props model action connect :: forall json action parent props model. FromJSON json => URL -> (WebSocket -> action) -> (Closed -> action) -> (Payload json -> action) -> (URL -> action) -> Effect parent props model action connect = URL -> (WebSocket -> action) -> (Closed -> action) -> (Payload json -> action) -> (URL -> action) -> Effect parent props model action forall json action parent props model. FromJSON json => URL -> (WebSocket -> action) -> (Closed -> action) -> (Payload json -> action) -> (URL -> action) -> Effect parent props model action websocketConnect ----------------------------------------------------------------------------- -- | <https://developer.mozilla.org/en-US/docs/Web/API/WebSocket> -- -- Establishes a t'WebSocket' server that assumes a JSON-encoded protocol. -- connectJSON :: FromJSON json => URL -- ^ URL endpoint for a t'WebSocket' connection -> (WebSocket -> action) -- ^ @onOpen@ callback w/ t'WebSocket' object for successful connection. t'WebSocket' is used here to send messages. -> (Closed -> action) -- ^ @onClosed@ method that is called when a t'WebSocket' connection has closed. -> (json -> action) -- ^ @onMessage@ is a callback invoked when a JSON-encoded message has been received from the t'WebSocket' server. -> (MisoString -> action) -- ^ Error message callback -> Effect parent props model action connectJSON :: forall json action parent props model. FromJSON json => URL -> (WebSocket -> action) -> (Closed -> action) -> (json -> action) -> (URL -> action) -> Effect parent props model action connectJSON = URL -> (WebSocket -> action) -> (Closed -> action) -> (json -> action) -> (URL -> action) -> Effect parent props model action forall json action parent props model. FromJSON json => URL -> (WebSocket -> action) -> (Closed -> action) -> (json -> action) -> (URL -> action) -> Effect parent props model action websocketConnectJSON ----------------------------------------------------------------------------- -- | <https://developer.mozilla.org/en-US/docs/Web/API/WebSocket> -- -- Establishes a t'WebSocket' server that assumes a text-encoded protocol. -- connectText :: URL -- ^ URL endpoint for a t'WebSocket' connection -> (WebSocket -> action) -- ^ @onOpen@ callback w/ t'WebSocket' object for successful connection. t'WebSocket' is used here to send messages. -> (Closed -> action) -- ^ @onClosed@ method that is called when a t'WebSocket' connection has closed. -> (MisoString -> action) -- ^ @onMessage@ is a callback invoked when a text-encoded message has been received from the t'WebSocket' server. -> (MisoString -> action) -- ^ Error message callback -> Effect parent props model action connectText :: forall action parent props model. URL -> (WebSocket -> action) -> (Closed -> action) -> (URL -> action) -> (URL -> action) -> Effect parent props model action connectText = URL -> (WebSocket -> action) -> (Closed -> action) -> (URL -> action) -> (URL -> action) -> Effect parent props model action forall action parent props model. URL -> (WebSocket -> action) -> (Closed -> action) -> (URL -> action) -> (URL -> action) -> Effect parent props model action websocketConnectText ----------------------------------------------------------------------------- -- | <https://developer.mozilla.org/en-US/docs/Web/API/WebSocket> -- -- Establishes a t'WebSocket' server that assumes a binary-encoded protocol. -- connectBLOB :: URL -- ^ URL endpoint for a t'WebSocket' connection -> (WebSocket -> action) -- ^ @onOpen@ callback w/ t'WebSocket' object for successful connection. t'WebSocket' is used here to send messages. -> (Closed -> action) -- ^ @onClosed@ method that is called when a t'WebSocket' connection has closed. -> (Blob -> action) -- ^ @onMessage@ is a callback invoked when a binary-encoded message has been received from the t'WebSocket' server. -> (MisoString -> action) -- ^ @onError@ callback -> Effect parent props model action connectBLOB :: forall action parent props model. URL -> (WebSocket -> action) -> (Closed -> action) -> (Blob -> action) -> (URL -> action) -> Effect parent props model action connectBLOB = URL -> (WebSocket -> action) -> (Closed -> action) -> (Blob -> action) -> (URL -> action) -> Effect parent props model action forall action parent props model. URL -> (WebSocket -> action) -> (Closed -> action) -> (Blob -> action) -> (URL -> action) -> Effect parent props model action websocketConnectBLOB ----------------------------------------------------------------------------- -- | <https://developer.mozilla.org/en-US/docs/Web/API/WebSocket> -- -- Establishes a t'WebSocket' server that assumes an t'ArrayBuffer' protocol. -- connectArrayBuffer :: URL -- ^ URL endpoint for a t'WebSocket' connection -> (WebSocket -> action) -- ^ @onOpen@ callback w/ t'WebSocket' object for successful connection. t'WebSocket' is used here to send messages. -> (Closed -> action) -- ^ @onClosed@ method that is called when a t'WebSocket' connection has closed. -> (ArrayBuffer -> action) -- ^ @onMessage@ is a callback invoked when an t'ArrayBuffer' message has been received from the t'WebSocket' server. -> (MisoString -> action) -- ^ @onError@ callback -> Effect parent props model action connectArrayBuffer :: forall action parent props model. URL -> (WebSocket -> action) -> (Closed -> action) -> (ArrayBuffer -> action) -> (URL -> action) -> Effect parent props model action connectArrayBuffer = URL -> (WebSocket -> action) -> (Closed -> action) -> (ArrayBuffer -> action) -> (URL -> action) -> Effect parent props model action forall action parent props model. URL -> (WebSocket -> action) -> (Closed -> action) -> (ArrayBuffer -> action) -> (URL -> action) -> Effect parent props model action websocketConnectArrayBuffer ----------------------------------------------------------------------------- -- | <https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send> -- -- @ -- -- data Person = Person { name :: MisoString, age :: Int } -- deriving (Show, Eq) -- -- instance ToJSON Person where -- toJSON (Person name age) = object [ "name" .= name, "age" .= age ] -- -- test :: WebSocket -> Effect parent props model action -- test connection = do -- sendJSON (connection :: WebSocket) (Person "alice" 42) -- sendJSON (connection :: WebSocket) (Person "bob" 42) -- -- @ -- sendJSON :: ToJSON json => WebSocket -- ^ t'WebSocket' descriptor required to send a message to the server. -> json -- ^ A JSON-encoded message -> Effect parent props model action sendJSON :: forall json parent props model action. ToJSON json => WebSocket -> json -> Effect parent props model action sendJSON WebSocket socket json x = WebSocket -> Payload json -> Effect parent props model action forall value parent props model action. ToJSON value => WebSocket -> Payload value -> Effect parent props model action websocketSend WebSocket socket (json -> Payload json forall value. value -> Payload value JSON json x) ----------------------------------------------------------------------------- -- | <https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send> sendBLOB :: WebSocket -- ^ t'WebSocket' descriptor required to send a message to the server. -> Blob -- ^ An t'Blob' payload to send to the t'WebSocket' server -> Effect parent props model action sendBLOB :: forall parent props model action. WebSocket -> Blob -> Effect parent props model action sendBLOB WebSocket socket Blob x = forall value parent props model action. ToJSON value => WebSocket -> Payload value -> Effect parent props model action websocketSend @() WebSocket socket (Blob -> Payload () forall value. Blob -> Payload value blob Blob x) ----------------------------------------------------------------------------- -- | <https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send> sendArrayBuffer :: WebSocket -- ^ t'WebSocket' descriptor required to send a message to the server. -> ArrayBuffer -- ^ An t'ArrayBuffer' payload to send to the t'WebSocket' server -> Effect parent props model action sendArrayBuffer :: forall parent props model action. WebSocket -> ArrayBuffer -> Effect parent props model action sendArrayBuffer WebSocket socket ArrayBuffer x = forall value parent props model action. ToJSON value => WebSocket -> Payload value -> Effect parent props model action websocketSend @() WebSocket socket (ArrayBuffer -> Payload () forall value. ArrayBuffer -> Payload value arrayBuffer ArrayBuffer x) ----------------------------------------------------------------------------- -- | <https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send> sendText :: WebSocket -- ^ t'WebSocket' descriptor required to send a message to the server. -> MisoString -- ^ A text payload to send to t'WebSocket' server -> Effect parent props model action sendText :: forall parent props model action. WebSocket -> URL -> Effect parent props model action sendText WebSocket socket URL x = forall value parent props model action. ToJSON value => WebSocket -> Payload value -> Effect parent props model action websocketSend @() WebSocket socket (URL -> Payload () forall value. URL -> Payload value TEXT URL x) ----------------------------------------------------------------------------- -- | <https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close> -- -- It is very important to close the t'WebSocket', otherwise leaks can occur. -- -- 'close' is a no-op if invoked multiple times. -- close :: WebSocket -- ^ t'WebSocket' descriptor required to close the socket on the server. -> Effect parent props model action close :: forall parent props model action. WebSocket -> Effect parent props model action close = WebSocket -> Effect parent props model action forall parent props model action. WebSocket -> Effect parent props model action websocketClose -----------------------------------------------------------------------------