As per the last post, justin’s simple-json is now in the purerl package set, which seemed to be the perfect opportunity to write a demo app with PureScript backend (purerl and cowboy) and PureScript frontend chatting over a websocket.

This will also be an opportunity to discuss some of the changes which make writing low-level bindings to OTP behaviours possible. Expect to see some cool stuff soon exposing some higher level (and likely more opinionated) bindings.

Server

Our demo app will be streaming load numbers, so we add the application os_mon and write some basic FFI to wrap cpu_sup:avg1

avg1_(Nothing, Just) -> fun () -> case cpu_sup:avg1() of {error, _} -> Nothing; Load when is_integer(Load) -> Just(Load) end end. foreign import avg1_ :: Maybe Int -> (Int -> Maybe Int) -> Effect (Maybe Int) avg1 :: Effect (Maybe Int) avg1 = avg1_ Nothing Just

Now we can write a websocket handler. A cowboy handler is a callback module with various mandatory and optional callbacks. To write a callback module in PureScript, we need to ensure that we write a module with top-level bindings with the correct names and types; the purescript-erl-cowboy library gives a few utilities to help get these types right, but ultimately this is rather fiddly and low-level, and we will ultimately want a higher-level API which allows us to treat handlers as composable values - the disconnect being that modules are not run-time value-level constructs.

A cowboy_websocket handler has 2 init functions: firstly a plain init that initialises state from initial config and switches to the websocket behavior (in common with other cowboy handlers), and secondly a websocket_init that performs initialisation that should be done in the context of the websocket process itself.

We’re not going to save any state or initial config, so we can define an init as follows:

type State = Unit init :: forall c. InitHandler c State init = mkEffectFn2 \req _ -> do log "init was called" pure $ initResult unit req

The pointless log call is included to illustrate the fact that these handler functions can be effectful things. Now, what ensures “the types line up”? Firstly, InitHandler is defined as

type InitHandler c s = EffectFn2 Req c (InitResult s)

and secondly the compiler guarantees that a top level binding which is an EffectFn*N* , Fn*N* or is a curried function of arity N is represented as an erlang function of arity N . The type EffectFnN is an arity- N erlang function (as in the JS backend though with a stronger notion of arity) and has no thunking, and this carries over to the top level definition. (I told you it was fiddly.) Ro cap it off, erl-cowboy has defined InitHandler c s to match the cowboy type init(cowboy_req:req(), Config) -> {cowboy_websocket, cowboy_req:req(), State} .

We now write a websocket_init function, which we’ll use to set off a timer to send updates. Define a quick

foreign import startInterval :: forall a. Int -> a -> Effect Unit

and we can simply have

data InfoMessage = Timer websocket_init :: WSInitHandler State websocket_init = mkEffectFn1 \s -> do startInterval 100 Timer pure $ okResult s

Now to the interesting bits. When a frame is recieved, websocket_handle is called, actually for this app we don’t care as we will only get some ping messages

websocket_handle :: forall f. FrameHandler f websocket_handle = mkEffectFn2 \frame state -> do log "got frame" pure $ okResult state

Now the interesting bit is the websocket_info handler, which will handle messages sent to our websocket handler process. The startInterval call above sends a message Timer of type InfoMessage , so we will define

websocket_info :: InfoHandler InfoMessage State

We are basically asserting here that nobody else will send a message of any other form to the process, this is morally in the “FFI has no guarantees” camp, here we only send the right messages with our FFI but if that invariant is not maintained, the process will crash.

So without further ado here’s the info handler:

websocket_info = mkEffectFn2 info1 where info1 :: InfoMessage -> State -> Effect (CallResult Unit) info1 msg state = do avg1' <- CPU.avg1 avg5' <- CPU.avg5 avg15' <- CPU.avg15 case avg1', avg5', avg15' of Just avg1, Just avg5, Just avg15 -> do let msg :: Message msg = { load: { avg1, avg5, avg15 }} let outFrames = singleton $ outFrame $ TextFrame $ SimpleJSON.writeJSON msg pure $ replyResult state outFrames _, _, _ -> do log "Some issue fetching load info" pure $ okResult state

So the interesting bit is the line defining outFrames . We wrap a string in a TextFrame to send, and encoding our message as JSON is sas simple as SimpleJSON.writeJSON . The Message type is defined as follows, and crucially this type is shared between server and client:

type Load = { avg1 :: Int, avg5 :: Int, avg15 :: Int } type Message = { load :: Load }

simple-json , it’s just that simple.

We’ll omit the code to stand up the cowboy server with routing etc., but you can find it in the repo .

Client

For a client we’ll use spork to render a few bits of HTML to the page (because really, who wants to use the DOM). Firstly, a little utility to create and subscribe to the socket, here using the purescript-web-socket low level WebSocket bindings.

createSocket :: String -> (String -> Effect Unit) -> Effect Unit createSocket url cb = do socket <- WS.create url [] listener <- EET.eventListener \ev -> for_ (ME.fromEvent ev) \msgEvent -> for_ (runExcept $ readString $ ME.data_ msgEvent) cb EET.addEventListener WSET.onMessage listener false (WS.toEventTarget socket) void $ setInterval 10000 $ WS.sendString socket "pingy"

Now the client - firstly the scaffolding to render our type to the page:

type Model = Load data Action = UpdateMessage Message update ∷ Model → Action → Model update _ = case _ of UpdateMessage { load } → load render ∷ Model → Html Action render { avg1, avg5, avg15 } = H.ul [] [ H.li [] [ H.text $ show avg1 ] , H.li [] [ H.text $ show avg5 ] , H.li [] [ H.text $ show avg15 ] ] app ∷ PureApp Model Action app = { update, render, init: { avg1: 0, avg5: 0, avg15: 0 } }

Nothing to see here, folks, spork makes it simple. We then subscribe to the websocket and push in messages:

main ∷ Effect Unit main = do appInstance <- PureApp.makeWithSelector app "#app" createSocket "ws://localhost:8082/ws" \json -> for_ (SimpleJSON.readJSON json) \msg -> do appInstance.push $ UpdateMessage msg appInstance.run

Again the connection is the use of the shared Message type and the use of SimpleJSON.readJSON . We decode JSON with simple-json on the javascript backend, while it was encoded with simple-json on the Erlang backend - the underlying JSON encoder/decoder was different, but we need not care.

Last words

I think this is pretty cool, but there’s some way to go from this to a more natural, type-safe version of the websocket handler shown here. This is something actively being worked on now, and there are definitely some better options for individual APIs (with different tradeoffs), but I think it’s still useful to see we can implement a callback module directly.

The code for this example is available in the repo .

本文前端(javascript)相关术语:javascript是什么意思 javascript下载 javascript权威指南 javascript基础教程 javascript 正则表达式 javascript设计模式 javascript高级程序设计 精通javascript javascript教程

代码区博客精选文章
分页:12
转载请注明
本文标题:PureScript-to-PureScript websockets with simple-json and low-level cowboy bindin ...
本站链接:https://www.codesec.net/view/620844.html


1.凡CodeSecTeam转载的文章,均出自其它媒体或其他官网介绍,目的在于传递更多的信息,并不代表本站赞同其观点和其真实性负责;
2.转载的文章仅代表原创作者观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,本站对该文以及其中全部或者部分内容、文字的真实性、完整性、及时性,不作出任何保证或承若;
3.如本站转载稿涉及版权等问题,请作者及时联系本站,我们会及时处理。
登录后可拥有收藏文章、关注作者等权限...
技术大类 技术大类 | 前端(javascript) | 评论(0) | 阅读(62)