Good library. I like the idea to map WS to a process. The WS library Iām using now doesnāt provide process abstraction so I have to implement it every time.
Thanks for sharing this with the community. I was looking for something exactly like this - Iām developing some API clients right now, and one of them needs to connect with a WebSocket. I guess this will help me a lot!
Awesome work, Im having some trouble though, how do i send json, im currently trying Client.send_frame(pid, {:text, "{'Page':{'url':'asdf'}}"}) but i get the response `Received Message: {āerrorā:{ācodeā:-32700,āmessageā:āMessage must be a valid JSONā}}`` from the connected serverā¦ any help would be appreciated
This release has some major API changes where callbacks are changed or removed without deprecation.
The affected callbacks are:
init/2 - Removed in favor of handle_connect/2.
handle_disconnect/2 - The first argument is now a map.
handle_connect_failure/2 - Removed with functionality rolled into handle_disconnect/2.
Please donāt be afraid to provide feedback. WebSockex is already meeting my needs, Iād like more information on what needs to change to meet the needs of others.
The full changelog for this release includes:
Major Changes
Moved all the WebSockex.Client module functionality into the base WebSockex module.
Roll handle_connect_failure functionality into handle_disconnect.
Roll init functionality into handle_connect
Detailed Changes
Roll init functionality into handle_connect
handle_connect will be invoked upon establishing any connection, i.e., the intial connection and when reconnecting.
The init callback is removed entirely.
Moved all the WebSockex.Client module functionality into the base WebSockex module.
Changed the Application module to WebSockex.Application.
Add WebSockex.start for non-linked processes.
Add async option to start and start_link.
Roll handle_connect_failure functionality into handle_disconnect.
The first parameter of handle_disconnect is now a map with the keys: :reason, :conn, and :attempt_number.
handle_disconnect now has another return option for when wanted to reconnect with different URI options or headers: {:reconnect, new_conn, new_state}
Added the :handle_initial_conn_failure option to the options for start and start_link that will allow handle_disconnect to be called if we can establish a connection during those functions.
When I realized, āOh right, this should play nice with OTP.ā I decided to learn what made a process āspecialā instead of just putting it in a GenServer.
There was a point where I was frustrated and started to roll it into a GenServer, but at that point I also realized the design choice that was frustrating me and changed it.
Sure, but first let me say there are two different websocket_client repos that I used.
The first one was the one listed on hex.pm(which is the one you linked). It implements gen_fsm and it more actively maintained. However, there are also a couple bugs. Most of them were already reported issues, but one in particular was that terminate was being called consistently in one of my cases. However it was fickle and I had problems reproducing it, but it was causing my tests to fail 1/15 times. So when I tried to jump and trace it, I was overwhelmed by the sheer number of things the repo was trying to do.
So I looked at the original fork. It didnāt have any of the bugs I experienced but also wasnāt an OTP special process and didnāt wait for the server to close the socket (as per the spec). I think I also had some problems with close frames that contained a payload. But this repo is no longer maintained in favor of the one above.
So, I decided to to write my own. I wanted it to be some where in between. Being compatible with OTP, but simple enough that someone could jump into and be able to submit simple bug-free code without being overwhelmed.
The remark that it implements special process by hand made me interested, so I had a look at the code. Here are some things I spotted, I hope you donāt mind.
you donāt handle the regular naming conventions - it would be nice to be able to pass the name option like with all the Elixirās stdlib behaviours. When you do this, there are some race conditions to consider, so I would advise looking at using the :gen module that powers all the Erlang behaviours (itās not documented publicly, but I saw many projects using it, Iām not sure how bad of an idea that is - but it definitely handles a lot of common scenarios).
whenever you call user module callbacks, they should be wrapped in a try and errors should be handled appropriately (e.g. you want to call the terminate callback in that case before dying).
itās nice to use :sys.handle_debug for any messages received and sent out (or any āmajorā events that happen in a process) - this is really useful when things go wrong.
in the receive loop, you probably want to look for the {:EXIT, parent, reason} message in case the user starts trapping exits in the process. The special process has to exit with the same reason as the parent in case that happens.
it looks like you donāt implement the system_code_change/4 callback
thereās an additional format_status/2 callback you can implement to control what information is returned on :sys.get_status and :sys.get_state used, for example, by observer.
I absolutely appreciate and encourage trying different things, and even overcomplicating the code for the purpose of learning and gaining a deeper understanding. That being said, if learning is the only motivation for using your own special process here, and assuming that this library is meant to be used by others and contributed to, I think it might be wise to switch to GenServer. Doing this would make it easier to solve (and in some cases automatically solve) issues mentioned by @michalmuskala.
It might also simplify some future additions. For example, I think that calls should be supported as well, and this would be pretty simple if the process was a GenServer. Making the interface of your behaviour more similar to GenServer by supporting timeouts and hibernate options in return tuples, as well as stop option would also be nice. Again, this should be fairly trivial if your behaviour was powered by GenServer.
I have similar sentiments about the current state of WS clients. About a year ago, I looked into a couple of them, and wasnāt really completely pleased with any. In the end, I settled for this branch. I like that your project starts with a clean slate, and has good test coverage. That seems very promising!
Awesome feedback, thanks a ton. A couple of other things I didnāt mention or am starting to remember now.
So the :gen module implements the call behavior. Which I had a really long thought process where I didnāt really want to have any synchronous calls because of the behavior of WebSockets and the way reconnects are implemented. That may change in the future but for now I donāt want any thing that can Timeout.
But I donāt mind adding a :name option. That actually doesnāt look too hard.
In the :gen module it looks like it checks during start and then again during the init function when it tries to register the process.
Ahh, good point. I did that for some of them, but I donāt think that itās tested and it isnāt consistent.
Yeah, that is me being lazy. I also need to add an error message on exit.
I totally didnāt know that. Thanks.
Oops. Thanks, incidentally I have no idea how to test this.
This made me remember why I didnāt use GenServer. I didnāt want those behaviors to be available because it could mess the handling of the control frames and I didnāt want to move the WebSocket into another child process.
I had the idea of writing another module based off GenServer that would wrap a WebSockex connection. This would make doing complex things like synchronous sends possible without messing too much with the flow that the WebSocket spec asks for.
The fact that call is synchronous for the caller, does not mean itās synchronous for the server. Using an explicit GenServer.reply/1 you can reply to the message before returning from the handle_call function, after or even from a completely different process.