Receive & send websocket messages through convenient observers. Even multiple observers on different endpoints!
💡Types of observers: Classic, Declarative, Bindable. Read about all of them below.
Built for Vapor4.
💡Vapor3 version is available in
vapor3branch and from1.0.0tag
If you have great ideas of how to improve this package write me (@iMike#3049) in Vapor's discord chat or just send pull request.
Edit your Package.swift
//add this repo to dependencies
.package(url: "https://github.com/MihaelIsaev/AwesomeWS.git", from: "2.0.0")
//and don't forget about targets
.target(name: "App", dependencies: [
.product(name: "WS", package: "AwesomeWS"),
.product(name: "Vapor", package: "vapor")
]),WS lib have .default WSID which represents DeclarativeObserver.
💡You can declare your own WSID with another type of observer and your custom class.
You can start working with it this easy way
app.ws.build(.default).serve()In this case it will start listening for websocket connections at /, but you can change it before you call .serve()
app.ws.build(.default).at("ws").serve()Ok now it is listening at /ws
Also you can protect your websocket endpoint with middlewares, e.g. you can check auth before connection will be established.
app.ws.build(.default).at("ws").middlewares(AuthMiddleware()).serve()Ok, looks good, but how to handle incoming data?
As we use .default WSID which represents Declarative observer we can handle incoming data like this
app.ws.build(.default).at("ws").middlewares(AuthMiddleware()).serve().onOpen { client in
print("client just connected \(client.id)")
}.onText { client, text in
print("client \(client.id) text: \(text)")
}there are also available: onClose, onPing, onPong, onBinary, onByteBuffer handlers.
💡Set
app.logger.logLevel = .infoorapp.logger.logLevel = .debugto see more info about connections
You should create new class which inherit from ClassicObserver
import WS
class MyClassicWebSocket: ClassicObserver {
override func on(open client: AnyClient) {}
override func on(close client: AnyClient) {}
override func on(text: String, client: AnyClient) {}
/// also you can override: `on(ping:)`, `on(pong:)`, `on(binary:)`, `on(byteBuffer:)`
}and you must declare a WSID for it
extension WSID {
static var myClassic: WSID<MyClassicWebSocket> { .init() }
}so then start serving it
app.ws.build(.myClassic).at("ws").serve()This kind of observer designed to send and receive events in special format, e.g. in JSON:
{ "event": "<event name>", "payload": <anything> }or just
{ "event": "<event name>" }💡By default lib uses
JSONEncoderandJSONDecoder, but you can replace them with anything else insetupmethod.
First of all declare any possible events in EID extension like this
struct Hello: Codable {
let firstName, lastName: String
}
struct Bye: Codable {
let firstName, lastName: String
}
extension EID {
static var hello: EID<Hello> { .init("hello") }
static var bye: EID<Bye> { .init("bye") }
// Use `EID<Nothing>` if you don't want any payload
}Then create your custom bindable observer class
class MyBindableWebsocket: BindableObserver {
// register all EIDs here
override func setup() {
bind(.hello, hello)
bind(.bye, bye)
// optionally setup here custom encoder/decoder
encoder = JSONEncoder() // e.g. with custom `dateEncodingStrategy`
decoder = JSONDecoder() // e.g. with custom `dateDecodingStrategy`
}
// hello EID handler
func hello(client: AnyClient, payload: Hello) {
print("Hello \(payload.firstName) \(payload.lastName)")
}
// bye EID handler
func bye(client: AnyClient, payload: Bye) {
print("Bye \(payload.firstName) \(payload.lastName)")
}
}declare a WSID
extension WSID {
static var myBindable: WSID<MyBindableWebsocket> { .init() }
}then start serving it
app.ws.build(.myBindable).at("ws").serve()💡Here you also could provide custom encoder/decoder e,g,
app.ws.build(.myBindable).at("ws").encoder(JSONEncoder()).encoder(JSONDecoder()).serve()
Data sending works through Sendable protocol, which have several methods
.send(text: <StringProtocol>) // send message with text
.send(bytes: <[UInt8]>) // send message with bytes
.send(data: <Data>) // send message with binary data
.send(model: <Encodable>) // send message with Encodable model
.send(model: <Encodable>, encoder: Encoder)
.send(event: <EID>) // send bindable event
.send(event: <EID>, payload: T?)all these methods returns
EventLoopFuture<Void>
Using methods listed above you could send messages to one or multiple clients.
client.send(...)client.broadcast.send(...)
client.broadcast.exclude(client).send(...) // excluding himself
req.ws(.mywsid).broadcast.send(...)client.broadcast.channels("news", "updates").send(...)
req.ws(.mywsid).broadcast.channels("news", "updates").send(...)e.g. you want to find all ws connections of the current user to send a message to all his devices
req.ws(.mywsid).broadcast.filter { client in
req.headers[.authorization].first == client.originalRequest.headers[.authorization].first
}.send(...)You could reach broadcast obejct on app.ws.observer(.mywsid) or req.ws(.mywsid).broadcast or client.broadcast.
This object is a builder, so using it you should filter recipients like this client.broadcast.one(...).two(...).three(...).send()
Available methods
.encoder(Encoder) // set custom data encoder
.exclude([AnyClient]) // exclude provided clients from clients
.filter((AnyClient) -> Bool) // filter clients by closure result
.channels([String]) // filter clients by provided channels
.subscribe([String]) // subscribe filtered clients to channels
.unsubscribe([String]) // unsubscribe filtered clients from channels
.disconnect() // disconnect filtered clients
.send(...) // send message to filtered clients
.count // number of filtered clientsclient.subscribe(to: ...) // will subscribe client to provided channelsTo subscribe to news and updates call it like this client.subscribe(to: "news", "updates")
client.unsubscribe(from: ...) // will unsubscribe client from provided channelsclient.channels // will return a list of client channelsIf you have only one observer in the app you can set it as default. It will give you ability to use it without providing its WSID all the time, so you will call just req.ws() instead of req.ws(.mywsid).
// configure.swift
app.ws.setDefault(.myBindable)Also you can set custom encoder/decoder for all the observers
// configure.swift
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .secondsSince1970
app.ws.encoder = encoder
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
app.ws.decoder = decoderAs you may see in every handler you always have client object. This object conforms to AnyClient protocol which contains useful things inside
variables
id- UUIDoriginalRequest- originalRequesteventLoop- nextEventLoopapplication- pointer toApplicationchannels- an array of channels that client subscribed tologger- pointer toLoggerobserver- this client's observersockets- original socket connection of the clientexchangeMode- client's observer exchange mode
conformance
Sendable- so you can use.send(...)Subscribable- so you can use.subscribe(...),.unsubscribe(...)Disconnectable- so you can call.disconnect()to disconnect that user
Original request gives you ability to e.g. determine connected user:
let user = try client.originalRequest.requireAuthenticated(User.self)You could use pure URLSession websockets functionality since iOS13, or for example you could use my CodyFire lib or classic Starscream lib
Use any lib which support pure websockets protocol, e.g. not SocketIO cause it uses its own protocol.
There are no examples for Vapor 4 yet unfortunately.
Please feel free to contact me in Vapor's discord my nickname is iMike#3049
Feel free to contribute!
