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
vapor3
branch and from1.0.0
tag
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 = .info
orapp.logger.logLevel = .debug
to 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
JSONEncoder
andJSONDecoder
, but you can replace them with anything else insetup
method.
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 clients
client.subscribe(to: ..., on: eventLoop) // will subscribe client to provided channels
To subscribe to news
and updates
call it like this client.subscribe(to: "news", "updates")
client.unsubscribe(from: ..., on: eventLoop) // will unsubscribe client from provided channels
client.channels // will return a list of client channels
If 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 = decoder
As 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
- originalRequest
eventLoop
- nextEventLoop
application
- pointer toApplication
channels
- an array of channels that client subscribed tologger
- pointer toLogger
observer
- 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)
Please take a look at this gist example
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!