diff --git a/Package.swift b/Package.swift index 8612548..691e893 100644 --- a/Package.swift +++ b/Package.swift @@ -1,28 +1,21 @@ -// swift-tools-version:4.2 -// The swift-tools-version declares the minimum version of Swift required to build this package. +// swift-tools-version:5.1 import PackageDescription let package = Package( name: "WS", + platforms: [ + .macOS(.v10_15) + ], products: [ - // Products define the executables and libraries produced by a package, and make them visible to other packages. - .library( - name: "WS", - targets: ["WS"]), + .library(name: "WS", targets: ["WS"]), ], dependencies: [ - // 💧 A server-side Swift web framework. - .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"), + .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0-beta.2"), + .package(url: "https://github.com/vapor/websocket-kit.git", from: "2.0.0-beta.2") ], targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages which this package depends on. - .target( - name: "WS", - dependencies: ["Vapor"]), - .testTarget( - name: "WSTests", - dependencies: ["WS", "Vapor"]), + .target(name: "WS", dependencies: ["Vapor", "WebSocketKit"]), + .testTarget(name: "WSTests", dependencies: ["WS", "WebSocketKit"]), ] ) diff --git a/README.md b/README.md index 70d01d2..2372290 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,22 @@ -[![Mihael Isaev](https://user-images.githubusercontent.com/1272610/50386554-68238c80-0702-11e9-88ad-965ebfd75812.png)](http://mihaelisaev.com) +[![Mihael Isaev](https://user-images.githubusercontent.com/1272610/72756525-ee045680-3be6-11ea-8a15-49414a453f8f.png)](http://mihaelisaev.com)

MIT License - Swift 4.2 - - - Twitter + Swift 5.1


+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. -Receive & send websocket messages through convenient controllers +Built for Vapor4. +> 💡Vapor3 version is available in `vapor3` branch and from `1.0.0` tag -**🚧 This project is under active development and API's and ideology may be changed or renamed until v1.0.0 🚧** +If you have great ideas of how to improve this package write me (@iMike#3049) in [Vapor's discord chat](http://vapor.team) or just send pull request. ### Install through Swift Package Manager @@ -23,204 +24,260 @@ Edit your `Package.swift` ```swift //add this repo to dependencies -.package(url: "https://github.com/MihaelIsaev/WS.git", from: "0.10.0") +.package(url: "https://github.com/MihaelIsaev/AwesomeWS.git", from: "2.0.0") //and don't forget about targets //"WS" ``` -### Setup in configure.swift +### How it works ? -```swift -import WS +### Declarative observer + +WS lib have `.default` WSID which represents `DeclarativeObserver`. +> 💡You can declare your own WSID with another type of observer and your custom class. -let ws = WS(at: "ws", protectedBy: [someMiddleware1, someMiddleware2], delegate: SOME_CONTROLLER) -// ws.logger.level = .debug -services.register(ws, as: WebSocketServer.self) +You can start working with it this easy way +```swift +app.ws.build(.default).serve() ``` -Let's take a look at WS initializations params. -First param is a path of endpoint where you'd like to listen for a websocket connection, in this example it is `/ws`, but you could provide any as you do it for enpoints in Vapor router. -Second param is optional, it's an array of middlewares which are protecting your websocket endpoint. e.g. for protecting b bearer token +In this case it will start listening for websocket connections at `/`, but you can change it before you call `.serve()` ```swift -let tokenAuthMiddleware = User.tokenAuthMiddleware() -let guardAuthMiddleware = User.guardAuthMiddleware() +app.ws.build(.default).at("ws").serve() ``` +Ok now it is listening at `/ws` -Third parameter is a controller object which will receive and handle all ws events like onOpen, onClose, onText, onBinary, onError. - -**⚠️ If you use middlewares please do not send any events to the server right in `onOpen` method. It is discussed in [#1](../../issues/1)** - -### Controllers - -#### Pure controller (classic) +Also you can protect your websocket endpoint with middlewares, e.g. you can check auth before connection will be established. +```swift +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 ```swift -let pureController = WSPureController() -pureController.onOpen = { client in - -} -pureController.onClose = { - -} -pureController.onError = { client, error in - -} -pureController.onBinary = { client, data in - -} -pureController.onText = { client, text in - +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` or `app.logger.logLevel = .debug` to see more info about connections -#### Custom controller - -You could create some class which inherit from `WSObserver` and describe your own logic. - -#### Bind controller +### Classic observer +You should create new class which inherit from `ClassicObserver` +```swift +import WS -You could create custom controller wich is inherit from `WSBindController` +class MyClassicWebSocket: ClassicObserver { + override func on(open client: AnyClient) {} + override func on(close client: AnyClient) {} -Let's take a look how it may look like + 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 ```swift -class WSController: WSBindController { - override func onOpen(_ client: WSClient) { - - } - override func onClose(_ client: WSClient) { - - } +extension WSID { + static var myClassic: WSID { .init() } } ``` -Then you could bind to some events, but you should describe these events first. +so then start serving it +```swift +app.ws.build(.myClassic).at("ws").serve() +``` + +### Bindable observer + +This kind of observer designed to send and receive events in special format, e.g. in JSON: +```json +{ "event": "", "payload": } +``` +or just +```json +{ "event": "" } +``` +> 💡By default lib uses `JSONEncoder` and `JSONDecoder`, but you can replace them with anything else in `setup` method. -e.g. we'd like to describe `message` event for our chat +First of all declare any possible events in `EID` extension like this ```swift -struct MessagePayload: Codable { - var fromUser: User.Public - var text: String +struct Hello: Codable { + let firstName, lastName: String } - -extension WSEventIdentifier { - static var message: WSEventIdentifier { return .init("message") } - // Payload is optional, so if you don't want any payload you could provide `NoPayload` as a paylaod class. +struct Bye: Codable { + let firstName, lastName: String +} +extension EID { + static var hello: EID { .init("hello") } + static var bye: EID { .init("bye") } + // Use `EID` if you don't want any payload } ``` -Ok, then now we can bind to this event in our custom bind controller: + +Then create your custom bindable observer class ```swift -class WSController: WSBindController { - override func onOpen(_ client: WSClient) { - bind(.message, message) +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` } - override func onClose(_ client: WSClient) { - + + // hello EID handler + func hello(client: AnyClient, payload: Hello) { + print("Hello \(payload.firstName) \(payload.lastName)") } -} -extension WSController { - func message(_ client: WSClient, _ payload: MessagePayload) { //or without payload if it's not needed! - //handle incoming message here + // bye EID handler + func bye(client: AnyClient, payload: Bye) { + print("Bye \(payload.firstName) \(payload.lastName)") } } ``` -Easy, right? - -Yeah, but you should know how its protocol works. - -`WSBindController` listening for `onText` and `onBinary`, it expect that incoming data is json object in this format: -```json -{ - "event": "some_event_name", - "payload": {} +declare a WSID +```swift +extension WSID { + static var myBindable: WSID { .init() } } ``` -or just (cause payload is optional) -```json -{ - "event": "some_event_name", -} +then start serving it +```swift +app.ws.build(.myBindable).at("ws").serve() ``` -It is actual for both sending and receiving events. +> 💡Here you also could provide custom encoder/decoder +> e,g, `app.ws.build(.myBindable).at("ws").encoder(JSONEncoder()).encoder(JSONDecoder()).serve()` -## WSClient +### How to send data -As you may see in every handler in both pure and bind controllers you always have `client` object. This object is `WSClient` class which contains a lot of useful things inside, like - -**variables** -- `cid` - UUID -- `req` - original Request -- `eventLoop` - EventLoop -- `channels` - a list of channels of that user +Data sending works through `Sendable` protocol, which have several methods +```swift +.send(text: ) // send message with text +.send(bytes: <[UInt8]>) // send message with bytes +.send(data: ) // send message with binary data +.send(model: ) // send message with Encodable model +.send(model: , encoder: Encoder) +.send(event: ) // send bindable event +.send(event: , payload: T?) +``` +> all these methods returns `EventLoopFuture` -**methods** -- subscribe(to channels: [String]) - use it to subscribe client to some channels -- unsubscribe(from channels: [String]) - use it to unsubscribe client from some channels -- broadcast - a lot of broadcast variations, just use autocompletion to determine needed one +Using methods listed above you could send messages to one or multiple clients. -More than that, it is `DatabaseConnectable`, so you could run your queries like this +#### To one client e.g. in `on(open:)` or `on(text:)` ```swift -User.query(on: client).all() +client.send(...) ``` -Original `req` gives you ability to e.g. determine connected user: +#### To all clients ```swift -let user = try client.req.requireAuthenticated(User.self) +client.broadcast.send(...) +client.broadcast.exclude(client).send(...) // excluding himself +req.ws(.mywsid).broadcast.send(...) ``` -Ok this is all about receiving websocket events.. what about sending? - -## Connected users +#### To clients in channels +```swift +client.broadcast.channels("news", "updates").send(...) +req.ws(.mywsid).broadcast.channels("news", "updates").send(...) +``` -In `WS` object which is available from any container through `.make` method you could find a set of `clients` +#### To custom filtered clients +e.g. you want to find all ws connections of the current user to send a message to all his devices ```swift -let ws = try req.make(WS.self) -ws.clients //this is a set of all connected clients +req.ws(.mywsid).broadcast.filter { client in + req.headers[.authorization].first == client.originalRequest.headers[.authorization].first +}.send(...) ``` -## Channels +### Broadcast -Any client may be subscribed or unsubscribed from some channels. +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 +```swift +.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 +``` -Channel is `WSChannel` obejct with unique `cid` which is `String`. +### Channels -You could subscribe/unsubscribe client to any channel by calling `client,subscribe(to:)` or `client,unsubscribe(from:)` +#### Subscribe +```swift +client.subscribe(to: ...) // will subscribe client to provided channels +``` +To subscribe to `news` and `updates` call it like this `client.subscribe(to: "news", "updates")` -And you could broadcast to channels by calling `ws.broadcast` +#### Unsubscribe +```swift +client.unsubscribe(from: ...) // will unsubscribe client from provided channels +``` +#### List +```swift +client.channels // will return a list of client channels +``` -## Sending events +### Defaults -You could get an instance of `WS` anywhere where you have `Container`. +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)`. +```swift +// configure.swift -### Broadcasting to some channel -e.g. in any request handler use broadcast method on `ws` object like this: +app.ws.setDefault(.myBindable) +``` +Also you can set custom encoder/decoder for all the observers ```swift -import WS +// configure.swift -func sampleGetRequestHandler(_ req: Request) throws -> Future { - let user = try req.requireAuthenticated(User.self) - let ws = try req.make(WS.self) - let payload = MessagePayload(fromUser: User.Public(user), text: "Some text") - return try ws.broadcast(asBinary: .message, payload, to: "some channel", on: req) - .transform(to: .ok) -} +let encoder = JSONEncoder() +encoder.dateEncodingStrategy = .secondsSince1970 +app.ws.encoder = encoder + +let decoder = JSONDecoder() +decoder.dateDecodingStrategy = .secondsSince1970 +app.ws.decoder = decoder ``` -### Sending some event to concrete client -e.g. in any request handler find needed client from `ws.clients` set and then use `emit` or `broadcast` method -```swift -import WS +### Client -func sampleGetRequestHandler(_ req: Request) throws -> Future { - let user = try req.requireAuthenticated(User.self) - let ws = try req.make(WS.self) - let payload = MessagePayload(fromUser: User.Public(user), text: "Some text") - return ws.clients.first!.emit("hello world", on: req).transform(to: .ok) //do not use force unwraiing in production! -} +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` - UUID +- `originalRequest` - original `Request` +- `eventLoop` - next `EventLoop` +- `application` - pointer to `Application` +- `channels` - an array of channels that client subscribed to +- `logger` - pointer to `Logger` +- `observer` - this client's observer +- `sockets` - original socket connection of the client +- `exchangeMode` - client's observer exchange mode + +**conformanses** +- `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: +```swift +let user = try client.originalRequest.requireAuthenticated(User.self) ``` ## How to connect from iOS, macOS, etc? -For example you could use [Starscream lib](https://github.com/daltoniam/Starscream) +You could use pure `URLSession` websockets functionality since iOS13, or for example you could use my [CodyFire lib](https://github.com/MihaelIsaev/CodyFire) or classic [Starscream lib](https://github.com/daltoniam/Starscream) ## How to connect from Android? @@ -228,13 +285,11 @@ Use any lib which support pure websockets protocol, e.g. not SocketIO cause it u ## Examples -Yeah, we have them! - -AlexoChat project [server](https://github.com/MihaelIsaev/AlexoChat) and [client](https://github.com/emvakar/Chat_client) +- [v1] AlexoChat project [server](https://github.com/MihaelIsaev/AlexoChat) and [client](https://github.com/emvakar/Chat_client) ## Contacts -Please feel free to contact me in Vapor's discord my nickname is `iMike` +Please feel free to contact me in Vapor's discord my nickname is `iMike#3049` ## Contribution diff --git a/Sources/WS/Channel/WSChannel+Hashable.swift b/Sources/WS/Channel/WSChannel+Hashable.swift deleted file mode 100644 index e3a3fa6..0000000 --- a/Sources/WS/Channel/WSChannel+Hashable.swift +++ /dev/null @@ -1,9 +0,0 @@ -extension WSChannel: Hashable { - public static func == (lhs: WSChannel, rhs: WSChannel) -> Bool { - return lhs.cid == rhs.cid - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(cid) - } -} diff --git a/Sources/WS/Channel/WSChannel+Set.swift b/Sources/WS/Channel/WSChannel+Set.swift deleted file mode 100644 index 0437e1a..0000000 --- a/Sources/WS/Channel/WSChannel+Set.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// WSChannel+Set.swift -// App -// -// Created by Mihael Isaev on 23/12/2018. -// - -import Foundation -import Vapor - -extension Set where Element == WSChannel { - func clients(in channels: [String]) -> Set { - return Set(filter { channels.contains($0.cid) }.flatMap { $0.clients }) - } -} diff --git a/Sources/WS/Channel/WSChannel.swift b/Sources/WS/Channel/WSChannel.swift deleted file mode 100644 index 6ac9ce5..0000000 --- a/Sources/WS/Channel/WSChannel.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation - -public class WSChannel { - public let cid: String - public var clients = Set() - init(_ uid: String) { - self.cid = uid - } -} diff --git a/Sources/WS/Client/WSClient+Broadcastable.swift b/Sources/WS/Client/WSClient+Broadcastable.swift deleted file mode 100644 index 51cf7c0..0000000 --- a/Sources/WS/Client/WSClient+Broadcastable.swift +++ /dev/null @@ -1,66 +0,0 @@ -import Foundation -import Vapor - -extension WSClient: WSBroadcastable { - @discardableResult - public func broadcast(_ text: String, to clients: Set, on container: Container) throws -> Future { - return try broadcaster().broadcast(text, to: clients, on: container) - } - - @discardableResult - public func broadcast(_ binary: Data, to clients: Set, on container: Container) throws -> Future { - return try broadcaster().broadcast(binary, to: clients, on: container) - } - - @discardableResult - public func broadcast(_ text: String, to channels: [String], on container: Container) throws -> Future { - return try broadcaster().broadcast(text, to: channels, on: container) - } - - @discardableResult - public func broadcast(_ text: String, to channel: String..., on container: Container) throws -> Future { - if channel.isEmpty { - return try broadcaster().broadcast(text, on: container) - } - return try broadcaster().broadcast(text, to: channel, on: container) - } - - @discardableResult - public func broadcast(_ binary: Data, to channels: [String], on container: Container) throws -> Future { - return try broadcaster().broadcast(binary, to: channels, on: container) - } - - @discardableResult - public func broadcast(_ binary: Data, to channel: String..., on container: Container) throws -> Future { - if channel.isEmpty { - return try broadcaster().broadcast(binary, on: container) - } - return try broadcaster().broadcast(binary, to: channel, on: container) - } - - @discardableResult - public func broadcast(asText event: WSEventIdentifier, _ payload: T? = nil, to channels: [String], on container: Container) throws -> Future { - return try broadcaster().broadcast(asText: event, payload, to: channels, on: container) - } - - @discardableResult - public func broadcast(asText event: WSEventIdentifier, _ payload: T? = nil, to channel: String..., on container: Container) throws -> Future { - if channel.isEmpty { - return try broadcaster().broadcast(asText: event, payload, on: container) - } - return try broadcaster().broadcast(asText: event, payload, to: channel, on: container) - } - - @discardableResult - public func broadcast(asBinary event: WSEventIdentifier, _ payload: T? = nil, to channels: [String], on container: Container) throws -> Future { - return try broadcaster().broadcast(asBinary: event, payload, to: channels, on: container) - } - - @discardableResult - public func broadcast(asBinary event: WSEventIdentifier, _ payload: T? = nil, to channel: String..., on container: Container) throws -> Future { - if channel.isEmpty { - return try broadcaster().broadcast(asBinary: event, payload, on: container) - } - return try broadcaster().broadcast(asBinary: event, payload, to: channel, on: container) - } -} diff --git a/Sources/WS/Client/WSClient+Emit.swift b/Sources/WS/Client/WSClient+Emit.swift deleted file mode 100644 index 0598e17..0000000 --- a/Sources/WS/Client/WSClient+Emit.swift +++ /dev/null @@ -1,63 +0,0 @@ -import Foundation -import Vapor - -extension WSClient { - /// Sends text-formatted data to the connected client. - @discardableResult - public func emit(_ text: S, on container: Container) -> Future where S: Collection, S.Element == Character { - let promise = container.eventLoop.newPromise(of: Void.self) - connection.send(text, promise: promise) - return promise.futureResult - } - - /// Sends binary-formatted data to the connected client. - @discardableResult - public func emit(_ binary: Data, on container: Container) -> Future { - let promise = container.eventLoop.newPromise(of: Void.self) - connection.send(binary: binary, promise: promise) - return promise.futureResult - } - - /// Sends text-formatted data to the connected client. - @discardableResult - public func emit(text: LosslessDataConvertible, on container: Container) -> Future { - let promise = container.eventLoop.newPromise(of: Void.self) - connection.send(text: text, promise: promise) - return promise.futureResult - } - - /// Sends binary-formatted data to the connected client. - @discardableResult - public func emit(binary: LosslessDataConvertible, on container: Container) -> Future { - let promise = container.eventLoop.newPromise(of: Void.self) - connection.send(binary: binary, promise: promise) - return promise.futureResult - } - - /// Sends raw-data to the connected client using the supplied WebSocket opcode. - @discardableResult - public func emit(raw data: LosslessDataConvertible, opcode: WebSocketOpcode, fin: Bool = true, on container: Container) -> Future { - let promise = container.eventLoop.newPromise(of: Void.self) - connection.send(raw: data, opcode: opcode, fin: fin, promise: promise) - return promise.futureResult - } - - /// Sends Codable model encoded to JSON string - @discardableResult - public func emit(asText event: WSEventIdentifier, payload: T? = nil, on container: Container) throws -> Future { - let jsonData = try JSONEncoder().encode(event) - guard let jsonString = String(data: jsonData, encoding: .utf8) else { - logger?.log(.error("Unable to preapare JSON string emit"), on: container) - throw WSError(reason: "Unable to preapare JSON string emit") - } - return emit(jsonString, on: container) - } - - /// Sends Codable model encoded to JSON binary - @discardableResult - public func emit(asBinary event: WSEventIdentifier, payload: T? = nil, on container: Container) throws -> Future { - let jsonEncoder = JSONEncoder() - jsonEncoder.dateEncodingStrategy = try container.make(WS.self).dateEncodingStrategy - return emit(try jsonEncoder.encode(WSOutgoingEvent(event.uid, payload: payload)), on: container) - } -} diff --git a/Sources/WS/Client/WSClient+Hashable.swift b/Sources/WS/Client/WSClient+Hashable.swift deleted file mode 100644 index 8e51bbd..0000000 --- a/Sources/WS/Client/WSClient+Hashable.swift +++ /dev/null @@ -1,9 +0,0 @@ -extension WSClient: Hashable { - public static func == (lhs: WSClient, rhs: WSClient) -> Bool { - return lhs.cid == rhs.cid - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(cid) - } -} diff --git a/Sources/WS/Client/WSClient+Set.swift b/Sources/WS/Client/WSClient+Set.swift deleted file mode 100644 index 537f86b..0000000 --- a/Sources/WS/Client/WSClient+Set.swift +++ /dev/null @@ -1,63 +0,0 @@ -import Foundation -import Vapor - -extension Set where Element == WSClient { - @discardableResult - public func broadcast(_ text: String, on container: Container) throws -> Future { - return map { $0.emit(text, on: container) }.flatten(on: container) - } - - @discardableResult - public func broadcast(_ binary: Data, on container: Container) throws -> Future { - return map { $0.emit(binary, on: container) }.flatten(on: container) - } - - @discardableResult - public func broadcast(_ text: String, to channel: String, on container: Container) throws -> Future { - return filter { $0.channels.contains(channel) }.map { $0.emit(text, on: container) }.flatten(on: container) - } - - @discardableResult - public func broadcast(_ binary: Data, to channel: String, on container: Container) throws -> Future { - return filter { $0.channels.contains(channel) }.map { $0.emit(binary, on: container) }.flatten(on: container) - } - - @discardableResult - public func broadcast(asText event: WSEventIdentifier, _ payload: T?, on container: Container) throws -> Future { - return try broadcast(asText: WSOutgoingEvent(event.uid, payload: payload), to: self, on: container) - } - - @discardableResult - public func broadcast(asText event: WSEventIdentifier, _ payload: T?, to channel: String, on container: Container) throws -> Future { - return try broadcast(asText: WSOutgoingEvent(event.uid, payload: payload), - to: filter { $0.channels.contains(channel) }, - on: container) - } - - @discardableResult - public func broadcast(asBinary event: WSEventIdentifier, _ payload: T?, on container: Container) throws -> Future { - return try broadcast(asBinary: WSOutgoingEvent(event.uid, payload: payload), to: self, on: container) - } - - @discardableResult - public func broadcast(asBinary event: WSEventIdentifier, _ payload: T?, to channel: String, on container: Container) throws -> Future { - return try broadcast(asBinary: WSOutgoingEvent(event.uid, payload: payload), - to: filter { $0.channels.contains(channel) }, - on: container) - } - - @discardableResult - func broadcast(asText event: WSOutgoingEvent, to clients: Set, on container: Container) throws -> Future { - let jsonData = try JSONEncoder().encode(event) - guard let jsonString = String(data: jsonData, encoding: .utf8) else { - throw WSError(reason: "Unable to preapare JSON string for broadcast message") - } - return clients.map { $0.emit(jsonString, on: container) }.flatten(on: container) - } - - @discardableResult - func broadcast(asBinary event: WSOutgoingEvent, to clients: Set, on container: Container) throws -> Future { - let jsonData = try JSONEncoder().encode(event) - return clients.map { $0.emit(jsonData, on: container) }.flatten(on: container) - } -} diff --git a/Sources/WS/Client/WSClient.swift b/Sources/WS/Client/WSClient.swift deleted file mode 100644 index c522d47..0000000 --- a/Sources/WS/Client/WSClient.swift +++ /dev/null @@ -1,63 +0,0 @@ -import Foundation -import Vapor -import WebSocket - -public class WSClient: Container, DatabaseConnectable { - public let cid = UUID() - let connection: WebSocket - public let req: Request - - public var config: Config { return req.config } - public var environment: Environment { return req.environment } - public var services: Services { return req.services } - public var serviceCache: ServiceCache { return req.serviceCache } - public var http: HTTPRequest { return req.http } - public var eventLoop: EventLoop { return req.eventLoop } - - public var channels = Set() - - weak var logger: WSLoggable? - weak var broadcastable: WSBroadcastable? - weak var channelable: WSChannelable? - - init (_ connection: WebSocket, _ req: Request, ws: (WSLoggable & WSBroadcastable & WSChannelable)) { - self.connection = connection - self.req = req - self.logger = ws - self.broadcastable = ws - self.channelable = ws - } - - //MARK: - - - public func subscribe(to channels: String...) { - subscribe(to: channels) - } - - public func subscribe(to channels: [String]) { - channelable?.subscribe(self, to: channels) - } - - public func unsubscribe(from channels: String...) { - unsubscribe(from: channels) - } - - public func unsubscribe(from channels: [String]) { - channelable?.unsubscribe(self, from: channels) - } - - //MARK: - WSBroadcastable - - func broadcaster() throws -> WSBroadcastable { - guard let broadcastable = broadcastable else { - throw WSError(reason: "Unable to unwrap broadcastable") - } - return broadcastable - } - - //MARK: - DatabaseConnectable - - public func databaseConnection(to database: DatabaseIdentifier?) -> Future { - return req.databaseConnection(to: database) - } -} diff --git a/Sources/WS/Controllers/WSBindController.swift b/Sources/WS/Controllers/WSBindController.swift deleted file mode 100644 index e9b9353..0000000 --- a/Sources/WS/Controllers/WSBindController.swift +++ /dev/null @@ -1,77 +0,0 @@ -import Foundation - -open class WSBindController: WSObserver { - typealias BindHandler = (WSClient, Data) -> Void - - var binds: [String: BindHandler] = [:] - - public func bind(_ identifier: WSEventIdentifier

, _ handler: @escaping (WSClient, P?) -> Void) { - binds[identifier.uid] = { [weak self] client, data in - do { - let res = try JSONDecoder().decode(WSEvent

.self, from: data) - handler(client, res.payload) - } catch { - self?.logger?.log(.error(String(describing: error)), on: client.req) - } - } - } - - public func bind(_ identifier: WSEventIdentifier

, _ handler: @escaping (WSClient, P) -> Void) { - binds[identifier.uid] = { [weak self] client, data in - do { - let res = try JSONDecoder().decode(WSEvent

.self, from: data) - guard let payload = res.payload else { throw WSError(reason: "Unable to unwrap payload") } - handler(client, payload) - } catch { - self?.logger?.log(.error(String(describing: error)), on: client.req) - } - } - } - - /// Calls when a new client connects. Override this function to handle `onOpen`. - open func onOpen(_ client: WSClient) {} - - /// Calls when a client disconnects. Override this function to handle `onClose`. - open func onClose(_ client: WSClient) {} - - public override func wsOnOpen(_ ws: WS, _ client: WSClient) -> Bool { - let result = super.wsOnOpen(ws, client) - if result { - onOpen(client) - } - return result - } - - public override func wsOnClose(_ ws: WS, _ client: WSClient) { - super.wsOnClose(ws, client) - onClose(client) - } - - public override func wsOnText(_ ws: WS, _ client: WSClient, _ text: String) { - super.wsOnText(ws, client, text) - if let data = text.data(using: .utf8) { - proceedData(ws, client, data: data) - } - } - - public override func wsOnBinary(_ ws: WS, _ client: WSClient, _ data: Data) { - super.wsOnBinary(ws, client, data) - proceedData(ws, client, data: data) - } - - func proceedData(_ ws: WS, _ client: WSClient, data: Data) { - do { - let prototype = try JSONDecoder().decode(WSEventPrototype.self, from: data) - switch prototype.event { - case "join": ws.joining(client, data: data, on: client.req) - case "leave": ws.leaving(client, data: data, on: client.req) - default: break - } - if let bind = binds.first(where: { $0.0 == prototype.event }) { - bind.value(client, data) - } - } catch { - logger?.log(.error(String(describing: error)), on: client.req) - } - } -} diff --git a/Sources/WS/Controllers/WSPureController.swift b/Sources/WS/Controllers/WSPureController.swift deleted file mode 100644 index d35f0ec..0000000 --- a/Sources/WS/Controllers/WSPureController.swift +++ /dev/null @@ -1,43 +0,0 @@ -import Foundation - -open class WSPureController: WSObserver { - public typealias OnOpenHandler = (WSClient) -> Void - public var onOpen: OnOpenHandler? - - public typealias OnCloseHandler = () -> Void - public var onClose: OnCloseHandler? - - public typealias OnTextHandler = (WSClient, String) -> Void - public var onText: OnTextHandler? - - public typealias OnBinaryHandler = (WSClient, Data) -> Void - public var onBinary: OnBinaryHandler? - - public typealias OnErrorHandler = (WSClient, Error) -> Void - public var onError: OnErrorHandler? - - override public func wsOnOpen(_ ws: WS, _ client: WSClient) -> Bool { - if super.wsOnOpen(ws, client) { - onOpen?(client) - return true - } - return false - } - - override public func wsOnClose(_ ws: WS, _ client: WSClient) { - super.wsOnClose(ws, client) - onClose?() - } - - override public func wsOnText(_ ws: WS, _ client: WSClient, _ text: String) { - onText?(client, text) - } - - override public func wsOnBinary(_ ws: WS, _ client: WSClient, _ data: Data) { - onBinary?(client, data) - } - - override public func wsOnError(_ ws: WS, _ client: WSClient, _ error: Error) { - onError?(client, error) - } -} diff --git a/Sources/WS/Enums/ExchangeMode.swift b/Sources/WS/Enums/ExchangeMode.swift new file mode 100644 index 0000000..86c9396 --- /dev/null +++ b/Sources/WS/Enums/ExchangeMode.swift @@ -0,0 +1,12 @@ +public enum ExchangeMode { + /// all the messages will be sent and received as `text` + case text + + /// all the messages will be sent and received as `binary data` + case binary + + /// default mode + /// `binary data` will be sent and received as is + /// `text` will be sent and received as is + case both +} diff --git a/Sources/WS/Extensions/Application+Configurator.swift b/Sources/WS/Extensions/Application+Configurator.swift new file mode 100644 index 0000000..0bcb753 --- /dev/null +++ b/Sources/WS/Extensions/Application+Configurator.swift @@ -0,0 +1,31 @@ +import Vapor + +extension Application { + /// Configure WS through this variable + /// + /// Declare WSID in extension + /// ```swift + /// extension WSID { + /// static var my: WSID { .init() } + /// } + /// ``` + /// + /// Configure endpoint and start it serving + /// ```swift + /// app.ws.build(.my).at("ws").middlewares(...).serve() + /// app.ws.setDefault(.my) + /// ``` + /// + /// Use it later on `Request` + /// ```swift + /// req.ws().send(...) + /// req.ws(.my).send(...) + /// ``` + /// or `Application` + /// ```swift + /// app.ws.observer().send(...) + /// app.ws.observer(.my).send(...) + /// ``` + /// + public var ws: Configurator { .init(self) } +} diff --git a/Sources/WS/Extensions/HTTPServerConfiguration+Address.swift b/Sources/WS/Extensions/HTTPServerConfiguration+Address.swift new file mode 100644 index 0000000..e85f386 --- /dev/null +++ b/Sources/WS/Extensions/HTTPServerConfiguration+Address.swift @@ -0,0 +1,8 @@ +import Vapor + +extension HTTPServer.Configuration { + var address: String { + let scheme = tlsConfiguration == nil ? "http" : "https" + return "\(scheme)://\(hostname):\(port)" + } +} diff --git a/Sources/WS/Extensions/Request+Observer.swift b/Sources/WS/Extensions/Request+Observer.swift new file mode 100644 index 0000000..1f23199 --- /dev/null +++ b/Sources/WS/Extensions/Request+Observer.swift @@ -0,0 +1,9 @@ +import Vapor + +extension Request { + /// Default websocket observer + public func ws() -> AnyObserver { application.ws.observer() } + + /// Selected websocket observer + public func ws(_ wsid: WSID) -> Observer { application.ws.observer(wsid) } +} diff --git a/Sources/WS/Models/Configurator.swift b/Sources/WS/Models/Configurator.swift new file mode 100644 index 0000000..41d0269 --- /dev/null +++ b/Sources/WS/Models/Configurator.swift @@ -0,0 +1,98 @@ +import Vapor + +public struct Configurator { + let application: Application + + init (_ application: Application) { + self.application = application + } + + // MARK: - Build + + /// Websocket endpoint builder. + /// Don't forget to call `.serve()` in the end. + public func build(_ wsid: WSID) -> EndpointBuilder { + .init(application, wsid) + } + + // MARK: - Observer + + /// Returns default observer. + /// Works only after `.build()`, otherwise fatal error. + public func observer() -> AnyObserver { + var anywsid: AnyWSID? = application.ws.default + if anywsid == nil, let key = application.wsStorage.items.values.first?.key { + anywsid = _WSID(key: key) + application.logger.warning("[⚡️] 🚩 Default websocket observer is nil. Use app.ws.setDefault(...). Used first available websocket.") + } + guard let wsid = anywsid else { + fatalError("[⚡️] 🚩Default websocket observer is nil. Use app.ws.default(...)") + } + guard let observer = application.wsStorage[wsid.key] else { + fatalError("[⚡️] 🚩Unable to get websocket observer with key `\(wsid.key)`") + } + return observer + } + + /// Returns observer for WSID. + /// Works only after `.build()`, otherwise fatal error. + public func observer(_ wsid: WSID) -> Observer { + guard let observer = application.wsStorage[wsid.key] as? Observer else { + fatalError("[⚡️] 🚩Websokcet with key `\(wsid.key)` is not running. Use app.ws.build(...).serve()") + } + return observer + } + + // MARK: - Default WSID storage + + /// Saves WSID as default. + /// After that you could call just `req.ws().send(...)` without providing WSID. + public func setDefault(_ wsid: WSID) { + self.default = wsid + } + + struct DefaultWSIDKey: StorageKey { + typealias Value = AnyWSID + } + + var `default`: AnyWSID? { + get { + application.storage[DefaultWSIDKey.self] + } + nonmutating set { + application.storage[DefaultWSIDKey.self] = newValue + } + } + + // MARK: - Default Encoder + + struct DefaultEncoderKey: StorageKey { + typealias Value = Encoder + } + + /// Default encoder for all the observers, if `nil` then `JSONEncoder` is used. + public var encoder: Encoder? { + get { + application.storage[DefaultEncoderKey.self] + } + nonmutating set { + application.storage[DefaultEncoderKey.self] = newValue + } + } + + // MARK: - Default Decoder + + struct DefaultDecoderKey: StorageKey { + typealias Value = Decoder + } + + /// Default encoder for all the observers, if `nil` then `JSONEncoder` is used. + public var decoder: Decoder? { + get { + application.storage[DefaultDecoderKey.self] + } + nonmutating set { + application.storage[DefaultDecoderKey.self] = newValue + } + } +} diff --git a/Sources/WS/Models/EID.swift b/Sources/WS/Models/EID.swift new file mode 100644 index 0000000..ab97f58 --- /dev/null +++ b/Sources/WS/Models/EID.swift @@ -0,0 +1,29 @@ +import Foundation + +/// Event identifier model +/// +/// Extend it to declare your websocket events +/// ```swift +/// extension EID { +/// static var userOnline: EID { .init("userOnline") } +/// } +/// ``` +public struct EID: Equatable, Hashable, CustomStringConvertible, ExpressibleByStringLiteral { + /// The unique id. + public let id: String + + /// See `CustomStringConvertible`. + public var description: String { + return id + } + + /// Create a new `EventIdentifier`. + public init(_ id: String) { + self.id = id + } + + /// See `ExpressibleByStringLiteral`. + public init(stringLiteral value: String) { + self.init(value) + } +} diff --git a/Sources/WS/Models/Event.swift b/Sources/WS/Models/Event.swift new file mode 100644 index 0000000..4a7fafb --- /dev/null +++ b/Sources/WS/Models/Event.swift @@ -0,0 +1,14 @@ +import Foundation + +struct Event: Codable { + public let event: String + public let payload: P? + public init (event: String, payload: P? = nil) { + self.event = event + self.payload = payload + } +} + +struct EventPrototype: Codable { + public var event: String +} diff --git a/Sources/WS/Models/Nothing.swift b/Sources/WS/Models/Nothing.swift new file mode 100644 index 0000000..176b195 --- /dev/null +++ b/Sources/WS/Models/Nothing.swift @@ -0,0 +1,2 @@ +/// Dummy model for EIDs without payload +public struct Nothing: Codable {} diff --git a/Sources/WS/Models/OriginalRequest.swift b/Sources/WS/Models/OriginalRequest.swift new file mode 100644 index 0000000..4cf2ab8 --- /dev/null +++ b/Sources/WS/Models/OriginalRequest.swift @@ -0,0 +1,71 @@ +//import Vapor +//import NIO +// +///// Represent an original HTTP request of WebSocket client connection +//public struct OriginalRequest: CustomStringConvertible { +// /// The HTTP method for this request. +// /// +// /// httpReq.method = .GET +// /// +// public let method: HTTPMethod +// +// /// The URL used on this request. +// public let url: URI +// +// /// The version for this HTTP request. +// public let version: HTTPVersion +// +// /// The header fields for this HTTP request. +// /// The `"Content-Length"` and `"Transfer-Encoding"` headers will be set automatically +// /// when the `body` property is mutated. +// public let headers: HTTPHeaders +// +// // MARK: Metadata +// +// /// Route object we found for this request. +// /// This holds metadata that can be used for (for example) Metrics. +// /// +// /// req.route?.description // "GET /hello/:name" +// /// +// public let route: Route? +// +// // MARK: Content +// +// public let query: URLQueryContainer +// +// public let content: ContentContainer +// +// public let body: Request.Body +// +// /// Get and set `HTTPCookies` for this `HTTPRequest` +// /// This accesses the `"Cookie"` header. +// public let cookies: HTTPCookies +// +// /// See `CustomStringConvertible` +// public let description: String +// +// public let remoteAddress: SocketAddress? +// +// public let eventLoop: EventLoop +// +// public let parameters: Parameters +// +// public let userInfo: [AnyHashable: Any] +// +// init(_ request: Request) { +// method = request.method +// url = request.url +// version = request.version +// headers = request.headers +// route = request.route +// query = request.query +// content = request.content +// body = request.body +// cookies = request.cookies +// description = request.description +// remoteAddress = request.remoteAddress +// eventLoop = request.eventLoop +// parameters = request.parameters +// userInfo = request.userInfo +// } +//} diff --git a/Sources/WS/Models/WSID.swift b/Sources/WS/Models/WSID.swift new file mode 100644 index 0000000..63d2c4e --- /dev/null +++ b/Sources/WS/Models/WSID.swift @@ -0,0 +1,22 @@ +import Vapor + +public protocol AnyWSID { + var key: String { get } +} + +struct _WSID: AnyWSID { + let key: String +} + +public struct WSID: AnyWSID { + public let key: String + + public init(_ key: String? = nil) { + self.key = key ?? String(describing: Observer.self) + } +} + +/// Set WSIDs in your app exactly the same way +extension WSID { + public static var `default`: WSID { .init("ws") } +} diff --git a/Sources/WS/Objects/Client.swift b/Sources/WS/Objects/Client.swift new file mode 100644 index 0000000..8c50e5a --- /dev/null +++ b/Sources/WS/Objects/Client.swift @@ -0,0 +1,69 @@ +import Foundation +import Vapor +import NIOWebSocket + +class Client: _AnyClient { + /// See `AnyClient` + public let id: UUID = .init() + public let originalRequest: Request + public let application: Application + + /// See `Loggable` + public let logger: Logger + + /// See `_Sendable` + let observer: AnyObserver + let _observer: _AnyObserver + let sockets: [WebSocketKit.WebSocket] + + /// See `AnyClient` + public internal(set) var channels: Set = [] + + /// See `Subscribable` + var clients: [_AnyClient] { [self] } + + init (_ observer: _AnyObserver, _ request: Vapor.Request, _ socket: WebSocketKit.WebSocket, logger: Logger) { + self.observer = observer + self._observer = observer + self.originalRequest = request + self.application = request.application + self.logger = logger + self.sockets = [socket] + } +} + +/// See `Sendable` + +extension Client { + public func send(text: S) -> EventLoopFuture where S : Collection, S.Element == Character { + _send(text: text) + } + + public func send(bytes: [UInt8]) -> EventLoopFuture { + _send(bytes: bytes) + } + + public func send(data: Data) -> EventLoopFuture where Data : DataProtocol { + _send(data: data) + } + + public func send(data: Data, opcode: WebSocketOpcode) -> EventLoopFuture where Data: DataProtocol { + _send(data: data, opcode: opcode) + } + + public func send(model: C) -> EventLoopFuture where C: Encodable { + _send(model: model) + } + + public func send(model: C, encoder: Encoder) -> EventLoopFuture where C: Encodable { + _send(model: model, encoder: encoder) + } + + public func send(event: EID) -> EventLoopFuture { + _send(event: event, payload: nil) + } + + public func send(event: EID, payload: T?) -> EventLoopFuture { + _send(event: event, payload: payload) + } +} diff --git a/Sources/WS/Observers/BaseObserver.swift b/Sources/WS/Observers/BaseObserver.swift new file mode 100644 index 0000000..a252d29 --- /dev/null +++ b/Sources/WS/Observers/BaseObserver.swift @@ -0,0 +1,36 @@ +import Foundation +import Vapor + +open class BaseObserver { + public let key: String + public let path: String + public let logger: Logger + public let application: Application + public let exchangeMode: ExchangeMode + public var encoder: Encoder? + public var decoder: Decoder? + + public internal(set) var clients: [AnyClient] = [] + var _clients: [_AnyClient] = [] + + public required init (app: Application, key: String, path: String, exchangeMode: ExchangeMode) { + self.application = app + self.logger = app.logger + self.key = key + self.path = path.count > 0 ? path : "/" + self.exchangeMode = exchangeMode + setup() + } + + open func setup() {} + + // MARK: see `AnyObserver` + + open func on(open client: AnyClient) {} + open func on(close client: AnyClient) {} + open func on(ping client: AnyClient) {} + open func on(pong client: AnyClient) {} + open func on(text: String, client: AnyClient) {} + open func on(byteBuffer: ByteBuffer, client: AnyClient) {} + open func on(data: Data, client: AnyClient) {} +} diff --git a/Sources/WS/Observers/BindableObserver.swift b/Sources/WS/Observers/BindableObserver.swift new file mode 100644 index 0000000..ecb2c28 --- /dev/null +++ b/Sources/WS/Observers/BindableObserver.swift @@ -0,0 +1,54 @@ +import Foundation +import Vapor +import NIOWebSocket + +open class BindableObserver: BaseObserver, Bindable, _Bindable { + var binds: [String : BindHandler] = [:] + + public func bind

(_ identifier: EID

, _ handler: @escaping (AnyClient) -> Void) where P: Codable { + _bind(identifier, handler) + } + + public func bindOptional

(_ identifier: EID

, _ handler: @escaping (AnyClient, P?) -> Void) where P : Codable { + _bindOptional(identifier, handler) + } + + public func bind

(_ identifier: EID

, _ handler: @escaping (AnyClient, P) -> Void) where P : Codable { + _bind(identifier, handler) + } +} + +/// See `Sendable` +extension BindableObserver { + public func send(text: S) -> EventLoopFuture where S : Collection, S.Element == Character { + _send(text: text) + } + + public func send(bytes: [UInt8]) -> EventLoopFuture { + _send(bytes: bytes) + } + + public func send(data: Data) -> EventLoopFuture where Data : DataProtocol { + _send(data: data) + } + + public func send(data: Data, opcode: WebSocketOpcode) -> EventLoopFuture where Data: DataProtocol { + _send(data: data, opcode: opcode) + } + + public func send(model: C) -> EventLoopFuture where C: Encodable { + _send(model: model) + } + + public func send(model: C, encoder: Encoder) -> EventLoopFuture where C: Encodable { + _send(model: model, encoder: encoder) + } + + public func send(event: EID) -> EventLoopFuture { + _send(event: event, payload: nil) + } + + public func send(event: EID, payload: T?) -> EventLoopFuture { + _send(event: event, payload: payload) + } +} diff --git a/Sources/WS/Observers/ClassicObserver.swift b/Sources/WS/Observers/ClassicObserver.swift new file mode 100644 index 0000000..dd450fb --- /dev/null +++ b/Sources/WS/Observers/ClassicObserver.swift @@ -0,0 +1,41 @@ +import Foundation +import Vapor +import NIOWebSocket + +open class ClassicObserver: BaseObserver, _AnyObserver, AnyObserver {} + +/// See `Sendable` + +extension ClassicObserver { + public func send(text: S) -> EventLoopFuture where S : Collection, S.Element == Character { + _send(text: text) + } + + public func send(bytes: [UInt8]) -> EventLoopFuture { + _send(bytes: bytes) + } + + public func send(data: Data) -> EventLoopFuture where Data : DataProtocol { + _send(data: data) + } + + public func send(data: Data, opcode: WebSocketOpcode) -> EventLoopFuture where Data: DataProtocol { + _send(data: data, opcode: opcode) + } + + public func send(model: C) -> EventLoopFuture where C: Encodable { + _send(model: model) + } + + public func send(model: C, encoder: Encoder) -> EventLoopFuture where C: Encodable { + _send(model: model, encoder: encoder) + } + + public func send(event: EID) -> EventLoopFuture { + _send(event: event, payload: nil) + } + + public func send(event: EID, payload: T?) -> EventLoopFuture { + _send(event: event, payload: payload) + } +} diff --git a/Sources/WS/Observers/DeclarativeObserver.swift b/Sources/WS/Observers/DeclarativeObserver.swift new file mode 100644 index 0000000..9cb5cf7 --- /dev/null +++ b/Sources/WS/Observers/DeclarativeObserver.swift @@ -0,0 +1,43 @@ +import Foundation +import Vapor +import NIOWebSocket + +open class DeclarativeObserver: BaseObserver, _Declarativable, Declarativable { + public internal(set) var handlers: DeclarativeHandlers = .init() +} + +/// See `Sendable` + +extension DeclarativeObserver { + public func send(text: S) -> EventLoopFuture where S : Collection, S.Element == Character { + _send(text: text) + } + + public func send(bytes: [UInt8]) -> EventLoopFuture { + _send(bytes: bytes) + } + + public func send(data: Data) -> EventLoopFuture where Data : DataProtocol { + _send(data: data) + } + + public func send(data: Data, opcode: WebSocketOpcode) -> EventLoopFuture where Data: DataProtocol { + _send(data: data, opcode: opcode) + } + + public func send(model: C) -> EventLoopFuture where C: Encodable { + _send(model: model) + } + + public func send(model: C, encoder: Encoder) -> EventLoopFuture where C: Encodable { + _send(model: model, encoder: encoder) + } + + public func send(event: EID) -> EventLoopFuture { + _send(event: event, payload: nil) + } + + public func send(event: EID, payload: T?) -> EventLoopFuture { + _send(event: event, payload: payload) + } +} diff --git a/Sources/WS/Protocols/AnyClient.swift b/Sources/WS/Protocols/AnyClient.swift new file mode 100644 index 0000000..4727957 --- /dev/null +++ b/Sources/WS/Protocols/AnyClient.swift @@ -0,0 +1,33 @@ +import Foundation +import Vapor +import NIOWebSocket + +public protocol AnyClient: Broadcastable, Disconnectable, Subscribable, Sendable { + var id: UUID { get } + var application: Application { get } + var eventLoop: EventLoop { get } + var originalRequest: Request { get } + var channels: Set { get } + var sockets: [WebSocket] { get } + var observer: AnyObserver { get } +} + +internal protocol _AnyClient: AnyClient, _Disconnectable, _Subscribable, _Sendable { + var _observer: _AnyObserver { get } + var channels: Set { get set } +} + +extension AnyClient { + public var eventLoop: EventLoop { application.eventLoopGroup.next() } + public var logger: Logger { application.logger } + + /// See `Broadcastable` + public var broadcast: Broadcaster { + observer.broadcast + } +} + +extension _AnyClient { + public var exchangeMode: ExchangeMode { observer.exchangeMode } + var _encoder: Encoder { observer._encoder } +} diff --git a/Sources/WS/Protocols/AnyObserver.swift b/Sources/WS/Protocols/AnyObserver.swift new file mode 100644 index 0000000..29db098 --- /dev/null +++ b/Sources/WS/Protocols/AnyObserver.swift @@ -0,0 +1,167 @@ +import Vapor + +public protocol AnyObserver: class, Broadcastable, CustomStringConvertible, Disconnectable, Sendable, Loggable { + var key: String { get } + var path: String { get } + + var application: Application { get } + var eventLoop: EventLoop { get } + var clients: [AnyClient] { get } + var encoder: Encoder? { get set } + var decoder: Decoder? { get set } + var exchangeMode: ExchangeMode { get } + + init (app: Application, key: String, path: String, exchangeMode: ExchangeMode) + + func setup() + + func on(open client: AnyClient) + func on(close client: AnyClient) + func on(ping client: AnyClient) + func on(pong client: AnyClient) + func on(text: String, client: AnyClient) + func on(byteBuffer: ByteBuffer, client: AnyClient) + func on(data: Data, client: AnyClient) +} + +internal protocol _AnyObserver: AnyObserver, _Disconnectable, _Sendable { + var _clients: [_AnyClient] { get set } + var _encoder: Encoder { get } + var _decoder: Decoder { get } + + func _on(open client: _AnyClient) + func _on(close client: _AnyClient) + func _on(ping client: _AnyClient) + func _on(pong client: _AnyClient) + func _on(text: String, client: _AnyClient) + func _on(byteBuffer: ByteBuffer, client: _AnyClient) + func _on(data: Data, client: _AnyClient) +} + +// MARK: - Default implementation + +extension AnyObserver { + public var eventLoop: EventLoop { application.eventLoopGroup.next() } + + var _encoder: Encoder { + if let encoder = self.encoder { + return encoder + } + if let encoder = application.ws.encoder { + return encoder + } + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .formatted(DefaultDateFormatter()) + return encoder + } + + var _decoder: Decoder { + if let decoder = self.decoder { + return decoder + } + if let decoder = application.ws.decoder { + return decoder + } + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .formatted(DefaultDateFormatter()) + return decoder + } + + func handle(_ req: Request, _ ws: WebSocketKit.WebSocket) { + guard let self = self as? _AnyObserver else { return } + self.handle(req, ws) + } + + /// See `Broadcastable` + + public var broadcast: Broadcaster { + .init(eventLoop: eventLoop, + clients: clients, + exchangeMode: exchangeMode, + logger: application.logger, + encoder: encoder, + defaultEncoder: application.ws.encoder) + } + + /// see `CustomStringConvertible` + public var description: String { + "\(String(describing: Self.self))(key: \"\(key)\", at: \"\(path)\")" + } +} + +extension _AnyObserver { + var clients: [AnyClient] { _clients } + var observer: _AnyObserver { self } + var sockets: [WebSocket] { _clients.flatMap { $0.sockets } } + + /// Internal handler + + func handle(_ req: Request, _ ws: WebSocketKit.WebSocket) { + let client = Client(self, req, ws, logger: logger) + _clients.append(client) + + _on(open: client) + on(open: client) + logger.info("[⚡️] 🟢 new connection \(client.id)") + + _ = ws.onClose.map { + self.logger.info("[⚡️] 🔴 connection closed \(client.id)") + self._clients.removeAll(where: { $0 === client }) + self._on(close: client) + self.on(close: client) + } + + ws.onPing { _ in + self.logger.debug("[⚡️] 🏓 ping \(client.id)") + self._on(ping: client) + self.on(ping: client) + } + + ws.onPong { _ in + self.logger.debug("[⚡️] 🏓 pong \(client.id)") + self._on(pong: client) + self.on(pong: client) + } + + ws.onText { _, text in + guard self.exchangeMode != .binary else { + self.logger.warning("[⚡️] ❗️📤❗️incoming text event has been rejected. Observer is in `binary` mode.") + return + } + self.logger.debug("[⚡️] 📥 \(client.id) text: \(text)") + self._on(text: text, client: client) + self.on(text: text, client: client) + } + + ws.onBinary { _, byteBuffer in + guard self.exchangeMode != .text else { + self.logger.warning("[⚡️] ❗️📤❗️incoming binary event has been rejected. Observer is in `text` mode.") + return + } + self.logger.debug("[⚡️] 📥 \(client.id) data: \(byteBuffer.readableBytes)") + self._on(byteBuffer: byteBuffer, client: client) + self.on(byteBuffer: byteBuffer, client: client) + guard byteBuffer.readableBytes > 0 else { return } + var bytes: [UInt8] = byteBuffer.getBytes(at: byteBuffer.readerIndex, length: byteBuffer.readableBytes) ?? [] + let data = Data(bytes: &bytes, count: byteBuffer.readableBytes) + self._on(data: data, client: client) + self.on(data: data, client: client) + } + } + + public func on(open client: AnyClient) {} + public func on(close client: AnyClient) {} + public func on(ping client: AnyClient) {} + public func on(pong client: AnyClient) {} + public func on(text: String, client: AnyClient) {} + public func on(byteBuffer: ByteBuffer, client: AnyClient) {} + public func on(data: Data, client: AnyClient) {} + + func _on(open client: _AnyClient) {} + func _on(close client: _AnyClient) {} + func _on(ping client: _AnyClient) {} + func _on(pong client: _AnyClient) {} + func _on(text: String, client: _AnyClient) {} + func _on(byteBuffer: ByteBuffer, client: _AnyClient) {} + func _on(data: Data, client: _AnyClient) {} +} diff --git a/Sources/WS/Protocols/Bindable.swift b/Sources/WS/Protocols/Bindable.swift new file mode 100644 index 0000000..9e8e25c --- /dev/null +++ b/Sources/WS/Protocols/Bindable.swift @@ -0,0 +1,109 @@ +import Foundation +import NIO + +typealias BindHandler = (AnyClient, Data) -> Void + +public protocol Bindable: AnyObserver { + /// Binds to event without payload + /// + /// - parameters: + /// - identifier: `EID` event identifier, declare it in extension + /// - handler: called when event happens + func bind

(_ identifier: EID

, _ handler: @escaping (AnyClient) -> Void) where P: Codable + + /// Binds to event with optional payload + /// + /// - parameters: + /// - identifier: `EID` event identifier, declare it in extension + /// - handler: called when event happens + func bindOptional

(_ identifier: EID

, _ handler: @escaping (AnyClient, P?) -> Void) where P: Codable + + /// Binds to event with required payload + /// + /// - parameters: + /// - identifier: `EID` event identifier, declare it in extension + /// - handler: called when event happens + func bind

(_ identifier: EID

, _ handler: @escaping (AnyClient, P) -> Void) where P: Codable +} + +internal protocol _Bindable: Bindable, _AnyObserver { + var binds: [String: BindHandler] { get set } +} + +extension _Bindable { + func _bind(_ identifier: EID

, _ handler: @escaping (AnyClient) -> Void) { + bindOptional(identifier) { client, _ in + handler(client) + } + } + + func _bindOptional(_ identifier: EID

, _ handler: @escaping (AnyClient, P?) -> Void) { + binds[identifier.id] = { client, data in + do { + let res = try self._decoder.decode(Event

.self, from: data) + handler(client, res.payload) + } catch { + self.unableToDecode(identifier.id, error) + } + } + } + + func _bind(_ identifier: EID

, _ handler: @escaping (AnyClient, P) -> Void) { + binds[identifier.id] = { client, data in + do { + let res = try self._decoder.decode(Event

.self, from: data) + if let payload = res.payload { + handler(client, payload) + } else { + self.logger.warning("[⚡️] ❗️📥❗️Unable to unwrap payload for event `\(identifier.id)`, because it is unexpectedly nil. Please use another `bind` method which support optional payload to avoid this message.") + } + } catch { + self.unableToDecode(identifier.id, error) + } + } + } + + private func unableToDecode(_ id: String, _ error: Error) { + switch logger.logLevel { + case .debug: logger.debug("[⚡️] ❗️📥❗️Undecodable incoming event `\(id)`: \(error)") + default: logger.error("[⚡️] ❗️📥❗️Unable to decode incoming event `\(id)`") + } + } +} + +extension _Bindable { + func _on(text: String, client: _AnyClient) { + if let data = text.data(using: .utf8) { + proceedData(client, data) + } + } + + func _on(byteBuffer: ByteBuffer, client: _AnyClient) { + guard byteBuffer.readableBytes > 0 else { return } + var bytes: [UInt8] = byteBuffer.getBytes(at: byteBuffer.readerIndex, length: byteBuffer.readableBytes) ?? [] + let data = Data(bytes: &bytes, count: byteBuffer.readableBytes) + proceedData(client, data) + } + + func _on(data: Data, client: _AnyClient) { + proceedData(client, data) + } + + private func proceedData(_ client: _AnyClient, _ data: Data) { + do { + let prototype = try _decoder.decode(EventPrototype.self, from: data) + if let bind = binds.first(where: { $0.0 == prototype.event }) { + bind.value(client, data) + } + } catch { + unableToDecode(error) + } + } + + private func unableToDecode(_ error: Error) { + switch logger.logLevel { + case .debug: logger.debug("[⚡️] ❗️📥❗️Unable to decode incoming event cause it doesn't conform to `EventPrototype` model: \(error)") + default: logger.error("[⚡️] ❗️📥❗️Unable to decode incoming event cause it doesn't conform to `EventPrototype` model") + } + } +} diff --git a/Sources/WS/Protocols/Broadcastable.swift b/Sources/WS/Protocols/Broadcastable.swift new file mode 100644 index 0000000..7b8889c --- /dev/null +++ b/Sources/WS/Protocols/Broadcastable.swift @@ -0,0 +1,6 @@ +import Foundation +import NIO + +public protocol Broadcastable { + var broadcast: Broadcaster { get } +} diff --git a/Sources/WS/Protocols/Declarativable.swift b/Sources/WS/Protocols/Declarativable.swift new file mode 100644 index 0000000..b4cab4c --- /dev/null +++ b/Sources/WS/Protocols/Declarativable.swift @@ -0,0 +1,153 @@ +import Vapor + +public typealias EmptyHandler = () -> Void +public typealias OpenCloseHandler = (AnyClient) -> Void +public typealias TextHandler = (AnyClient, String) -> Void +public typealias ByteBufferHandler = (AnyClient, ByteBuffer) -> Void +public typealias BinaryHandler = (AnyClient, Data) -> Void + +public class DeclarativeHandlers { + var openHander: OpenCloseHandler? + var closeHander: OpenCloseHandler? + var pingHander: OpenCloseHandler? + var pongHander: OpenCloseHandler? + var textHander: TextHandler? + var byteBufferHander: ByteBufferHandler? + var binaryHander: BinaryHandler? +} + +public protocol Declarativable: AnyObserver { + var handlers: DeclarativeHandlers { get } +} + +internal protocol _Declarativable: Declarativable, _AnyObserver { + var handlers: DeclarativeHandlers { get set } +} + +extension _Declarativable { + func _on(open client: _AnyClient) { + handlers.openHander?(client) + } + + func _on(close client: _AnyClient) { + handlers.closeHander?(client) + } + + func _on(ping client: _AnyClient) { + handlers.pingHander?(client) + } + + func _on(pong client: _AnyClient) { + handlers.pongHander?(client) + } + + func _on(text: String, client: _AnyClient) { + guard let handler = handlers.textHander else { + logger.warning("[⚡️] ❗️📥❗️ \(description) received `text` but handler is nil") + return + } + handler(client, text) + } + + func _on(byteBuffer: ByteBuffer, client: _AnyClient) { + guard let handler = handlers.byteBufferHander else { + logger.warning("[⚡️] ❗️📥❗️ \(description) received `byteBuffer` but handler is nil") + return + } + handler(client, byteBuffer) + } + + func _on(data: Data, client: _AnyClient) { + guard let handler = handlers.binaryHander else { + logger.warning("[⚡️] ❗️📥❗️ \(description) received `binary data` but handler is nil") + return + } + handler(client, data) + } +} + +extension Declarativable { + @discardableResult + public func onOpen(_ handler: @escaping OpenCloseHandler) -> Self { + handlers.openHander = handler + return self + } + + @discardableResult + public func onOpen(_ handler: @escaping EmptyHandler) -> Self { + handlers.openHander = { _ in handler() } + return self + } + + @discardableResult + public func onClose(_ handler: @escaping OpenCloseHandler) -> Self { + handlers.closeHander = handler + return self + } + + @discardableResult + public func onClose(_ handler: @escaping EmptyHandler) -> Self { + handlers.closeHander = { _ in handler() } + return self + } + + @discardableResult + public func onPing(_ handler: @escaping OpenCloseHandler) -> Self { + handlers.pingHander = handler + return self + } + + @discardableResult + public func onPing(_ handler: @escaping EmptyHandler) -> Self { + handlers.pingHander = { _ in handler() } + return self + } + + @discardableResult + public func onPong(_ handler: @escaping OpenCloseHandler) -> Self { + handlers.pongHander = handler + return self + } + + @discardableResult + public func onPong(_ handler: @escaping EmptyHandler) -> Self { + handlers.pongHander = { _ in handler() } + return self + } + + @discardableResult + public func onText(_ handler: @escaping TextHandler) -> Self { + handlers.textHander = handler + return self + } + + @discardableResult + public func onText(_ handler: @escaping EmptyHandler) -> Self { + handlers.textHander = { _,_ in handler() } + return self + } + + @discardableResult + public func onByteBuffer(_ handler: @escaping ByteBufferHandler) -> Self { + handlers.byteBufferHander = handler + return self + } + + @discardableResult + public func onByteBuffer(_ handler: @escaping EmptyHandler) -> Self { + handlers.byteBufferHander = { _,_ in handler() } + return self + } + + @discardableResult + public func onBinary(_ handler: @escaping BinaryHandler) -> Self { + handlers.binaryHander = handler + return self + } + + @discardableResult + public func onBinary(_ handler: @escaping EmptyHandler) -> Self { + handlers.binaryHander = { _,_ in handler() } + return self + } +} diff --git a/Sources/WS/Protocols/Decoder.swift b/Sources/WS/Protocols/Decoder.swift new file mode 100644 index 0000000..581adf9 --- /dev/null +++ b/Sources/WS/Protocols/Decoder.swift @@ -0,0 +1,7 @@ +import Foundation + +public protocol Decoder { + func decode(_ type: T.Type, from data: Data) throws -> T where T : Decodable +} + +extension JSONDecoder: Decoder {} diff --git a/Sources/WS/Protocols/Delegate.swift b/Sources/WS/Protocols/Delegate.swift new file mode 100644 index 0000000..9911810 --- /dev/null +++ b/Sources/WS/Protocols/Delegate.swift @@ -0,0 +1,9 @@ +//import Foundation +// +//public protocol Delegate { +// func wsOnOpen(_ ws: WS, _ client: Client) -> Bool +// func wsOnClose(_ ws: WS, _ client: Client) +// func wsOnText(_ ws: WS, _ client: Client, _ text: String) +// func wsOnBinary(_ ws: WS, _ client: Client, _ data: Data) +// func wsOnError(_ ws: WS, _ client: Client, _ error: Error) +//} diff --git a/Sources/WS/Protocols/Disconnectable.swift b/Sources/WS/Protocols/Disconnectable.swift new file mode 100644 index 0000000..31fa672 --- /dev/null +++ b/Sources/WS/Protocols/Disconnectable.swift @@ -0,0 +1,49 @@ +import Vapor +import NIOWebSocket + +public protocol Disconnectable { + @discardableResult + func disconnect() -> EventLoopFuture + @discardableResult + func disconnect(code: WebSocketErrorCode) -> EventLoopFuture +} + +internal protocol _Disconnectable: Disconnectable { + var eventLoop: EventLoop { get } + var sockets: [WebSocket] { get } +} + +extension _Disconnectable { + public func disconnect() -> EventLoopFuture { + _disconnect() + } + + public func disconnect(code: WebSocketErrorCode) -> EventLoopFuture { + _disconnect(code: code) + } + + func _disconnect() -> EventLoopFuture { + eventLoop.future().flatMap { + self._disconnect(code: .goingAway) + } + } + + func _disconnect(code: WebSocketErrorCode) -> EventLoopFuture { + guard sockets.count > 0 else { return eventLoop.future() } + return sockets.map { + $0.close(code: code) + }.flatten(on: eventLoop) + } +} + +// MARK: - EventLoopFuture + +extension EventLoopFuture: Disconnectable where Value: Disconnectable { + public func disconnect() -> EventLoopFuture { + flatMap { $0.disconnect() } + } + + public func disconnect(code: WebSocketErrorCode) -> EventLoopFuture { + flatMap { $0.disconnect(code: code) } + } +} diff --git a/Sources/WS/Protocols/Encoder.swift b/Sources/WS/Protocols/Encoder.swift new file mode 100644 index 0000000..a191583 --- /dev/null +++ b/Sources/WS/Protocols/Encoder.swift @@ -0,0 +1,7 @@ +import Foundation + +public protocol Encoder { + func encode(_ value: T) throws -> Data where T : Encodable +} + +extension JSONEncoder: Encoder {} diff --git a/Sources/WS/Protocols/Loggable.swift b/Sources/WS/Protocols/Loggable.swift new file mode 100644 index 0000000..792d6c0 --- /dev/null +++ b/Sources/WS/Protocols/Loggable.swift @@ -0,0 +1,5 @@ +import Logging + +public protocol Loggable { + var logger: Logger { get } +} diff --git a/Sources/WS/Protocols/Sendable.swift b/Sources/WS/Protocols/Sendable.swift new file mode 100644 index 0000000..004d051 --- /dev/null +++ b/Sources/WS/Protocols/Sendable.swift @@ -0,0 +1,153 @@ +import Vapor +import NIOWebSocket + +public protocol Sendable { + @discardableResult + func send(text: S) -> EventLoopFuture where S: Collection, S.Element == Character + @discardableResult + func send(bytes: [UInt8]) -> EventLoopFuture + @discardableResult + func send(data: Data) -> EventLoopFuture where Data: DataProtocol + @discardableResult + func send(data: Data, opcode: WebSocketOpcode) -> EventLoopFuture where Data: DataProtocol + @discardableResult + func send(model: C) -> EventLoopFuture where C: Encodable + @discardableResult + func send(model: C, encoder: Encoder) -> EventLoopFuture where C: Encodable + @discardableResult + func send(event: EID) -> EventLoopFuture + @discardableResult + func send(event: EID, payload: T?) -> EventLoopFuture +} + +internal protocol _Sendable: Sendable { + var eventLoop: EventLoop { get } + var exchangeMode: ExchangeMode { get } + var logger: Logger { get } + var _encoder: Encoder { get } + var sockets: [WebSocket] { get } +} + +extension _Sendable { + func _send(text: S) -> EventLoopFuture where S : Collection, S.Element == Character { + /// Send as `binary` instead + if exchangeMode == .binary { + self.logger.warning("[⚡️] ❗️📤❗️text will be automatically converted to binary data. Observer is in `binary` mode.") + return send(bytes: String(text).utf8.map{ UInt8($0) }) + } + /// Send as `text` + return eventLoop.future().map { + self.sockets.forEach { + self.logger.debug("[⚡️] 📤 text: \(text)") + $0.send(text) + } + } + } + + func _send(bytes: [UInt8]) -> EventLoopFuture { + /// Send as `text` instead + if exchangeMode == .text { + self.logger.warning("[⚡️] ❗️📤❗️bytes will be automatically converted to text. Observer is in `binary` mode.") + guard let text = String(bytes: bytes, encoding: .utf8) else { + self.logger.warning("[⚡️] ❗️📤❗️Unable to convert bytes to text. Observer is in `binary` mode.") + return eventLoop.future() + } + return send(text: text) + } + /// Send as `binary` + return eventLoop.future().map { + self.sockets.forEach { + self.logger.debug("[⚡️] 📤 bytes: \(bytes.count)") + $0.send(bytes) + } + } + } + + func _send(data: Data) -> EventLoopFuture where Data : DataProtocol { + send(data: data, opcode: .binary) + } + + func _send(data: Data, opcode: WebSocketOpcode) -> EventLoopFuture where Data: DataProtocol { + /// Send as `text` instead + if exchangeMode == .text { + self.logger.warning("[⚡️] ❗️📤❗️data will be automatically converted to text. Observer is in `text` mode.") + guard let text = String(bytes: data, encoding: .utf8) else { + self.logger.warning("[⚡️] ❗️📤❗️Unable to convert data to text. Observer is in `text` mode.") + return eventLoop.future() + } + return send(text: text) + } + /// Send as `binary` + return eventLoop.future().map { + self.sockets.forEach { + self.logger.debug("[⚡️] 📤 data: \(data.count)") + $0.send(raw: data, opcode: opcode) + } + } + } + + func _send(model: C) -> EventLoopFuture where C: Encodable { + send(model: model, encoder: _encoder) + } + + func _send(model: C, encoder: Encoder) -> EventLoopFuture where C: Encodable { + eventLoop.future().flatMapThrowing { + try encoder.encode(model) + }.flatMap { data -> EventLoopFuture in + if self.exchangeMode == .text { + return self.eventLoop.future(data) + } + return self.send(data: data).transform(to: data) + }.flatMap { + guard self.exchangeMode != .binary, + let text = String(data: $0, encoding: .utf8) else { + return self.eventLoop.future() + } + return self.send(text: text) + } + } + + func _send(event: EID) -> EventLoopFuture { + send(event: event, payload: nil) + } + + func _send(event: EID, payload: T?) -> EventLoopFuture { + send(model: Event(event: event.id, payload: payload)) + } +} + +// MARK: - EventLoopFuture + +extension EventLoopFuture: Sendable where Value: Sendable { + public func send(text: S) -> EventLoopFuture where S : Collection, S.Element == Character { + flatMap { $0.send(text: text) } + } + + public func send(bytes: [UInt8]) -> EventLoopFuture { + flatMap { $0.send(bytes: bytes) } + } + + public func send(data: Data) -> EventLoopFuture where Data : DataProtocol { + flatMap { $0.send(data: data) } + } + + public func send(data: Data, opcode: WebSocketOpcode) -> EventLoopFuture where Data : DataProtocol { + flatMap { $0.send(data: data, opcode: opcode) } + } + + public func send(model: C) -> EventLoopFuture where C : Encodable { + flatMap { $0.send(model: model) } + } + + public func send(model: C, encoder: Encoder) -> EventLoopFuture where C : Encodable { + flatMap { $0.send(model: model, encoder: encoder) } + } + + public func send(event: EID) -> EventLoopFuture where T : Decodable, T : Encodable { + flatMap { $0.send(event: event) } + } + + public func send(event: EID, payload: T?) -> EventLoopFuture where T : Decodable, T : Encodable { + flatMap { $0.send(event: event, payload: payload) } + } +} diff --git a/Sources/WS/Protocols/Subscribable.swift b/Sources/WS/Protocols/Subscribable.swift new file mode 100644 index 0000000..14de466 --- /dev/null +++ b/Sources/WS/Protocols/Subscribable.swift @@ -0,0 +1,68 @@ +import Vapor +import NIOWebSocket + +public protocol Subscribable { + @discardableResult + func subscribe(to channels: String...) -> EventLoopFuture + @discardableResult + func subscribe(to channels: [String]) -> EventLoopFuture + @discardableResult + func unsubscribe(from channels: String...) -> EventLoopFuture + @discardableResult + func unsubscribe(from channels: [String]) -> EventLoopFuture +} + +extension Subscribable { + public func subscribe(to channels: String...) -> EventLoopFuture { + subscribe(to: channels) + } + + public func unsubscribe(from channels: String...) -> EventLoopFuture { + unsubscribe(from: channels) + } +} + +internal protocol _Subscribable: class, Subscribable { + var eventLoop: EventLoop { get } + var clients: [_AnyClient] { get } +} + +extension _Subscribable { + public func subscribe(to channels: String...) -> EventLoopFuture { + subscribe(to: channels) + } + + public func subscribe(to channels: [String]) -> EventLoopFuture { + channels.forEach { channel in + self.clients.forEach { + $0.channels.insert(channel) + } + } + return eventLoop.future() + } + + public func unsubscribe(from channels: String...) -> EventLoopFuture { + unsubscribe(from: channels) + } + + public func unsubscribe(from channels: [String]) -> EventLoopFuture { + channels.forEach { channel in + self.clients.forEach { + $0.channels.remove(channel) + } + } + return eventLoop.future() + } +} + +// MARK: - EventLoopFuture + +extension EventLoopFuture: Subscribable where Value: Subscribable { + public func subscribe(to channels: [String]) -> EventLoopFuture { + flatMap { $0.subscribe(to: channels) } + } + + public func unsubscribe(from channels: [String]) -> EventLoopFuture { + flatMap { $0.unsubscribe(from: channels) } + } +} diff --git a/Sources/WS/Protocols/WSBroadcastable.swift b/Sources/WS/Protocols/WSBroadcastable.swift deleted file mode 100644 index 10714fa..0000000 --- a/Sources/WS/Protocols/WSBroadcastable.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation -import Vapor - -public protocol WSBroadcastable: class { - func broadcast(_ text: String, to clients: Set, on container: Container) throws -> Future - func broadcast(_ binary: Data, to clients: Set, on container: Container) throws -> Future - - //MARK: Text - func broadcast(_ text: String, to channels: [String], on container: Container) throws -> Future - func broadcast(_ text: String, to channel: String..., on container: Container) throws -> Future - - //MARK: Binary - func broadcast(_ binary: Data, to channels: [String], on container: Container) throws -> Future - func broadcast(_ binary: Data, to channel: String..., on container: Container) throws -> Future - - //MARK: Codable - func broadcast(asText event: WSEventIdentifier, _ payload: T?, to channels: [String], on container: Container) throws -> Future - func broadcast(asText event: WSEventIdentifier, _ payload: T?, to channel: String..., on container: Container) throws -> Future - func broadcast(asBinary event: WSEventIdentifier, _ payload: T?, to channels: [String], on container: Container) throws -> Future - func broadcast(asBinary event: WSEventIdentifier, _ payload: T?, to channel: String..., on container: Container) throws -> Future -} diff --git a/Sources/WS/Protocols/WSChannelable.swift b/Sources/WS/Protocols/WSChannelable.swift deleted file mode 100644 index ffca7af..0000000 --- a/Sources/WS/Protocols/WSChannelable.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// WSChannelable.swift -// WS -// -// Created by Mihael Isaev on 23/12/2018. -// - -import Foundation - -public protocol WSChannelable: class { - func subscribe(_ client: WSClient, to channels: [String]) - func unsubscribe(_ client: WSClient, from channels: [String]) -} diff --git a/Sources/WS/Protocols/WSControllerable.swift b/Sources/WS/Protocols/WSControllerable.swift deleted file mode 100644 index 4c6b893..0000000 --- a/Sources/WS/Protocols/WSControllerable.swift +++ /dev/null @@ -1,5 +0,0 @@ -import Foundation - -public protocol WSControllerable: WSDelegate { - var logger: WSLoggable? { get set } -} diff --git a/Sources/WS/Protocols/WSDelegate.swift b/Sources/WS/Protocols/WSDelegate.swift deleted file mode 100644 index 6069bca..0000000 --- a/Sources/WS/Protocols/WSDelegate.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation - -public protocol WSDelegate { - @discardableResult - func wsOnOpen(_ ws: WS, _ client: WSClient) -> Bool - func wsOnClose(_ ws: WS, _ client: WSClient) - func wsOnText(_ ws: WS, _ client: WSClient, _ text: String) - func wsOnBinary(_ ws: WS, _ client: WSClient, _ data: Data) - func wsOnError(_ ws: WS, _ client: WSClient, _ error: Error) -} diff --git a/Sources/WS/Protocols/WSLoggable.swift b/Sources/WS/Protocols/WSLoggable.swift deleted file mode 100644 index f369ee8..0000000 --- a/Sources/WS/Protocols/WSLoggable.swift +++ /dev/null @@ -1,5 +0,0 @@ -import Vapor - -public protocol WSLoggable: class { - func log(_ message: WSLogger.Message..., on container: Container) -} diff --git a/Sources/WS/Utilities/Broadcaster.swift b/Sources/WS/Utilities/Broadcaster.swift new file mode 100644 index 0000000..ce3e4d1 --- /dev/null +++ b/Sources/WS/Utilities/Broadcaster.swift @@ -0,0 +1,172 @@ +import WebSocketKit +import Logging +import Foundation +import NIOWebSocket + +public class Broadcaster: Disconnectable, _Disconnectable, Sendable, _Sendable, Subscribable { + let eventLoop: EventLoop + var clients: [AnyClient] + let exchangeMode: ExchangeMode + let logger: Logger + var encoder: Encoder? + let defaultEncoder: Encoder? + + var _encoder: Encoder { + if let encoder = self.encoder { + return encoder + } + if let encoder = self.defaultEncoder { + return encoder + } + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .formatted(DefaultDateFormatter()) + return encoder + } + + var excludedClients: Set = [] + + init (eventLoop: EventLoop, clients: [AnyClient], exchangeMode: ExchangeMode, logger: Logger, encoder: Encoder?, defaultEncoder: Encoder?) { + self.eventLoop = eventLoop + self.clients = clients + self.exchangeMode = exchangeMode + self.logger = logger + self.encoder = encoder + self.defaultEncoder = defaultEncoder + } + + /// Set custom data encoder + public func encoder(_ encoder: Encoder) -> Self { + self.encoder = encoder + return self + } + + /// Filtered clients + private var filteredClients: [AnyClient] { + clients.filter { client in + !excludedClients.contains { $0 == client.id } + } + } + + /// Cached filtered sockets + var cachedSockets: [WebSocket]? + + /// Filtered sockets + var sockets: [WebSocket] { + if let cachedSockets = cachedSockets { + return cachedSockets + } + let sockets = filteredClients.flatMap { $0.sockets } + cachedSockets = sockets + return sockets + } + + /// Quantity of filtered sockets + public var count: Int { cachedSockets?.count ?? sockets.count } + + /// Excludes provided clients from broadcast + public func exclude(_ clients: AnyClient...) -> Self { + exclude(clients) + } + + /// Exclude provided clients from recipients + public func exclude(_ clients: [AnyClient]) -> Self { + cachedSockets = nil + clients.forEach { + excludedClients.insert($0.id) + } + return self + } + + /// Filter recipients by closure result + public func filter(_ filter: @escaping (AnyClient) -> Bool) -> Self { + clients.removeAll { !filter($0) } + return self + } + + /// Filter recipients by closure result + public func filter(_ filter: @escaping (AnyClient) -> EventLoopFuture) -> EventLoopFuture { + var ids: Set = [] + return clients.map { client in + filter(client).map { leave in + if !leave { + ids.insert(client.id) + } + } + }.flatten(on: eventLoop).map { + self.clients.removeAll { ids.contains($0.id) } + }.transform(to: self) + } + + /// Filter recipients by provided channels + public func channels(_ channels: String...) -> Self { + self.channels(channels) + } + + /// Filter recipients by provided channels + public func channels(_ channels: [String]) -> Self { + clients.removeAll { + !$0.channels.contains(where: channels.contains) + } + return self + } + + /// See `Subscribable` + + /// Subscribe filtered clients to channels + public func subscribe(to channels: [String]) -> EventLoopFuture { + clients.map { + $0.subscribe(to: channels) + }.flatten(on: eventLoop) + } + + /// Unsubscribe filtered clients from channels + public func unsubscribe(from channels: [String]) -> EventLoopFuture { + clients.map { + $0.unsubscribe(from: channels) + }.flatten(on: eventLoop) + } + + /// See `Disconnectable` + + public func disconnect() -> EventLoopFuture { + _disconnect() + } + + public func disconnect(code: WebSocketErrorCode) -> EventLoopFuture { + _disconnect(code: code) + } + + /// See `Sendable` + + public func send(text: S) -> EventLoopFuture where S : Collection, S.Element == Character { + _send(text: text) + } + + public func send(bytes: [UInt8]) -> EventLoopFuture { + _send(bytes: bytes) + } + + public func send(data: Data) -> EventLoopFuture where Data : DataProtocol { + _send(data: data) + } + + public func send(data: Data, opcode: WebSocketOpcode) -> EventLoopFuture where Data: DataProtocol { + _send(data: data, opcode: opcode) + } + + public func send(model: C) -> EventLoopFuture where C: Encodable { + _send(model: model) + } + + public func send(model: C, encoder: Encoder) -> EventLoopFuture where C: Encodable { + _send(model: model, encoder: encoder) + } + + public func send(event: EID) -> EventLoopFuture { + _send(event: event, payload: nil) + } + + public func send(event: EID, payload: T?) -> EventLoopFuture { + _send(event: event, payload: payload) + } +} diff --git a/Sources/WS/WSDefaultDateFormatter.swift b/Sources/WS/Utilities/DefaultDateFormatter.swift similarity index 90% rename from Sources/WS/WSDefaultDateFormatter.swift rename to Sources/WS/Utilities/DefaultDateFormatter.swift index 77b678d..b9a1cf3 100644 --- a/Sources/WS/WSDefaultDateFormatter.swift +++ b/Sources/WS/Utilities/DefaultDateFormatter.swift @@ -1,13 +1,6 @@ -// -// WSDefaultDateFormatter.swift -// App -// -// Created by Mihael Isaev on 20/02/2019. -// - import Foundation -class WSDefaultDateFormatter: DateFormatter { +class DefaultDateFormatter: DateFormatter { func setup() { self.calendar = Calendar(identifier: .iso8601) self.locale = Locale(identifier: "en_US_POSIX") diff --git a/Sources/WS/Utilities/EndpointBuilder.swift b/Sources/WS/Utilities/EndpointBuilder.swift new file mode 100644 index 0000000..bc7700f --- /dev/null +++ b/Sources/WS/Utilities/EndpointBuilder.swift @@ -0,0 +1,100 @@ +import Vapor + +public class EndpointBuilder { + let application: Application + let wsid: WSID + + var path: [PathComponent] = [] + var middlewares: [Middleware] = [] + var exchangeMode: ExchangeMode = .both + var encoder: Encoder? + var decoder: Decoder? + + init (_ application: Application, _ wsid: WSID) { + self.application = application + self.wsid = wsid + } + + /// Path where websocket should listen to + public func at(_ path: PathComponent...) -> Self { + at(path) + } + + /// Path where websocket should listen to + public func at(_ path: [PathComponent]) -> Self { + self.path.append(contentsOf: path) + return self + } + + /// Middlewares to protect websocket endpoint + public func middlewares(_ middlewares: Middleware...) -> Self { + self.middlewares(middlewares) + } + + /// Middlewares to protect websocket endpoint + public func middlewares(_ middlewares: [Middleware]) -> Self { + self.middlewares.append(contentsOf: middlewares) + return self + } + + /// Observer data exchange mode. + /// Can be `text`, `binary` or `both`. + /// It is `both` by default. + public func mode(_ mode: ExchangeMode) -> Self { + exchangeMode = mode + return self + } + + /// Custom observer outgoing data encoder. `JSONEncoder` by default. May be needed for `Bindable` observer. + public func encoder(_ value: Encoder) -> Self { + encoder = value + return self + } + + /// Custom observer incoming data decoder. `JSONDecoder` by default. May be needed for `Bindable` observer. + public func decoder(_ value: Decoder) -> Self { + decoder = value + return self + } + + /// Starts websocket to listen on configured enpoint + @discardableResult + public func serve() -> Observer { + let observer = Observer.init(app: application, key: wsid.key, path: path.string, exchangeMode: exchangeMode) + + if let encoder = encoder { + observer.encoder = encoder + } + if let decoder = decoder { + observer.decoder = decoder + } + + application.wsStorage[wsid.key] = observer + WSRoute(root: application.grouped(middlewares), path: path).webSocket(onUpgrade: observer.handle) + + let observerType = String(describing: Observer.self) + application.logger.notice("[⚡️] 🚀 \(observerType) starting on \(application.server.configuration.address)/\(path.string)") + + return observer + } +} + +fileprivate final class WSRoute: RoutesBuilder { + /// Router to cascade to. + let root: RoutesBuilder + + /// Additional components. + let path: [PathComponent] + + /// Creates a new `PathGroup`. + init(root: RoutesBuilder, path: [PathComponent]) { + self.root = root + self.path = path + } + + /// See `HTTPRoutesBuilder`. + func add(_ route: Route) { + route.path = self.path + route.path + self.root.add(route) + } +} diff --git a/Sources/WS/Utilities/Storage.swift b/Sources/WS/Utilities/Storage.swift new file mode 100644 index 0000000..b83c421 --- /dev/null +++ b/Sources/WS/Utilities/Storage.swift @@ -0,0 +1,42 @@ +import Foundation +import Vapor + +class _Storage { + var items: [String: AnyObserver] = [:] + + struct Key: StorageKey { + typealias Value = _Storage + } + + subscript(_ member: String) -> AnyObserver? { + get { + return items[member] + } + set { + if let nv = newValue { + guard items[member] == nil else { return } + items[member] = nv + } else { + items.removeValue(forKey: member) + } + } + } +} + +extension Application { + var wsStorage: _Storage { + get { + if let ws = storage[_Storage.Key.self] { + return ws + } else { + logger.debug("[⚡️] 📦 Storage initialized") + let storage = _Storage() + self.wsStorage = storage + return storage + } + } + set { + storage[_Storage.Key.self] = newValue + } + } +} diff --git a/Sources/WS/WS/WS+Broadcast.swift b/Sources/WS/WS/WS+Broadcast.swift deleted file mode 100644 index df53d61..0000000 --- a/Sources/WS/WS/WS+Broadcast.swift +++ /dev/null @@ -1,99 +0,0 @@ -import Foundation -import Vapor - -extension WS: WSBroadcastable { - @discardableResult - public func broadcast(_ text: String, to clients: Set, on container: Container) throws -> Future { - return clients.map { $0.emit(text, on: container) }.flatten(on: container) - } - - @discardableResult - public func broadcast(_ binary: Data, to clients: Set, on container: Container) throws -> Future { - return clients.map { $0.emit(binary, on: container) }.flatten(on: container) - } - - @discardableResult - public func broadcast(_ text: String, to channels: [String], on container: Container) throws -> Future { - return try broadcast(text, to: channels, on: container) - } - - @discardableResult - public func broadcast(_ text: String, to channel: String..., on container: Container) throws -> Future { - if channel.isEmpty { - return try broadcast(text, to: clients, on: container) - } - return try broadcast(text, to: channel, on: container) - } - - @discardableResult - public func broadcast(_ binary: Data, to channels: [String], on container: Container) throws -> Future { - return try broadcast(binary, to: channels, on: container) - } - - @discardableResult - public func broadcast(_ binary: Data, to channel: String..., on container: Container) throws -> Future { - if channel.isEmpty { - return try broadcast(binary, to: clients, on: container) - } - return try broadcast(binary, to: channel, on: container) - } - - @discardableResult - public func broadcast(asText event: WSEventIdentifier, _ payload: T? = nil, to channels: [String], on container: Container) throws -> Future { - return try broadcast(asText: WSOutgoingEvent(event.uid, payload: payload), to: channels, on: container) - } - - @discardableResult - public func broadcast(asText event: WSEventIdentifier, _ payload: T? = nil, to channel: String..., on container: Container) throws -> Future { - if channel.isEmpty { - return try broadcast(asText: WSOutgoingEvent(event.uid, payload: payload), to: clients, on: container) - } - return try broadcast(asText: WSOutgoingEvent(event.uid, payload: payload), to: channel, on: container) - } - - @discardableResult - public func broadcast(asBinary event: WSEventIdentifier, _ payload: T? = nil, to channels: [String], on container: Container) throws -> Future { - return try broadcast(asBinary: WSOutgoingEvent(event.uid, payload: payload), to: channels, on: container) - } - - @discardableResult - public func broadcast(asBinary event: WSEventIdentifier, _ payload: T? = nil, to channel: String..., on container: Container) throws -> Future { - if channel.isEmpty { - return try broadcast(asBinary: WSOutgoingEvent(event.uid, payload: payload), to: clients, on: container) - } - return try broadcast(asBinary: WSOutgoingEvent(event.uid, payload: payload), to: channel, on: container) - } - - @discardableResult - func broadcast(asText event: WSOutgoingEvent, to channels: [String], on container: Container) throws -> Future { - let clients = self.channels.clients(in: channels) - return try broadcast(asText: event, to: clients, on: container) - } - - @discardableResult - func broadcast(asText event: WSOutgoingEvent, to clients: Set, on container: Container) throws -> Future { - let jsonData = try JSONEncoder().encode(event) - guard let jsonString = String(data: jsonData, encoding: .utf8) else { - throw WSError(reason: "Unable to preapare JSON string for broadcast message") - } - return try clients.broadcast(jsonString, on: container) - } - - @discardableResult - func broadcast(asBinary event: WSOutgoingEvent, to channels: [String], on container: Container) throws -> Future { - let clients = self.channels.clients(in: channels) - return try broadcast(asBinary: event, to: clients, on: container) - } - - @discardableResult - func broadcast(asBinary event: WSOutgoingEvent, to clients: Set, on container: Container) throws -> Future { - let jsonEncoder = JSONEncoder() - jsonEncoder.dateEncodingStrategy = try container.make(WS.self).dateEncodingStrategy - let jsonData = try jsonEncoder.encode(event) - print("clients.count: \(clients.count)") - for c in clients { - print("client: \(c)") - } - return try clients.broadcast(jsonData, on: container) - } -} diff --git a/Sources/WS/WS/WS+Channelable.swift b/Sources/WS/WS/WS+Channelable.swift deleted file mode 100644 index 1f56c8a..0000000 --- a/Sources/WS/WS/WS+Channelable.swift +++ /dev/null @@ -1,25 +0,0 @@ -extension WS: WSChannelable { - public func subscribe(_ client: WSClient, to channels: [String]) { - channels.forEach { ch in - client.channels.insert(ch) - if let channel = self.channels.first(where: { $0.cid == ch }) { - channel.clients.insert(client) - } else { - let channel = WSChannel(ch) - channel.clients.insert(client) - if !self.channels.contains(channel) { - self.channels.insert(channel) - } - } - } - } - - public func unsubscribe(_ client: WSClient, from channels: [String]) { - channels.forEach { ch in - client.channels.remove(ch) - if let channel = self.channels.first(where: { $0.cid == ch }) { - channel.clients.remove(client) - } - } - } -} diff --git a/Sources/WS/WS/WS+Channels.swift b/Sources/WS/WS/WS+Channels.swift deleted file mode 100644 index 1e978e5..0000000 --- a/Sources/WS/WS/WS+Channels.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Foundation -import Vapor - -extension WS { - func decodeEvent(by identifier: WSEventIdentifier

, with data: Data) throws -> WSEvent

{ - return try JSONDecoder().decode(WSEvent

.self, from: data) - } - - func joining(_ client: WSClient, data: Data, on container: Container) { - if let event = try? decodeEvent(by: .join, with: data), let payload = event.payload { - let cid = payload.channel.uuidString - let channel = channels.first { $0.cid == cid } ?? channels.insert(WSChannel(cid)).memberAfterInsert - channel.clients.insert(client) - client.channels.insert(cid) - channels.insert(channel) - - logger.log(.info("➡️ Some client has joined some channel"), - .debug("➡️ Client \(client.cid) has joined channel \(cid)"), on: container) - } - } - - func leaving(_ client: WSClient, data: Data, on container: Container) { - if let event = try? decodeEvent(by: .leave, with: data), let payload = event.payload { - let cid = payload.channel.uuidString - client.channels.remove(cid) - channels.first { $0.cid == cid }?.clients.remove(client) - logger.log(.info("⬅️ Some client has left some channel"), - .debug("⬅️ Client \(client.cid) has left channel \(cid)"), on: container) - } - } -} diff --git a/Sources/WS/WS/WS+Loggable.swift b/Sources/WS/WS/WS+Loggable.swift deleted file mode 100644 index 6f3f80a..0000000 --- a/Sources/WS/WS/WS+Loggable.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Vapor - -extension WS: WSLoggable { - public func log(_ message: WSLogger.Message..., on container: Container) { - logger.log(message, on: container) - } -} diff --git a/Sources/WS/WS/WS+Request.swift b/Sources/WS/WS/WS+Request.swift deleted file mode 100644 index 2b88497..0000000 --- a/Sources/WS/WS/WS+Request.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation -import Vapor - -extension WS { - public func requireClientByAuthToken(on req: Request) throws -> WSClient { - guard let token = req.http.headers[.authorization].first else { - throw WSError(reason: "Unable to get Authorization token from Request") - } - if let client = clientsCache[token] { - return client - } - guard let client = clients.first(where: { $0.http.headers[.authorization].first == token }) else { - throw WSError(reason: "Unable to find websocket client with Authorization token from Request") - } - clientsCache[token] = client - return client - } -} diff --git a/Sources/WS/WS/WS+WebSocketServer.swift b/Sources/WS/WS/WS+WebSocketServer.swift deleted file mode 100644 index 0a7a029..0000000 --- a/Sources/WS/WS/WS+WebSocketServer.swift +++ /dev/null @@ -1,34 +0,0 @@ -import Foundation -import Vapor - -extension WS { - public func webSocketShouldUpgrade(for request: Request) -> HTTPHeaders? { - return server.webSocketShouldUpgrade(for: request) - } - - public func webSocketOnUpgrade(_ webSocket: WebSocket, for request: Request) { - let success: () throws -> Void = { - self.server.webSocketOnUpgrade(webSocket, for: request) - } - do { - var middlewares = self.middlewares - if middlewares.count == 0 { - try success() - } else { - var iterate: () throws -> Void = {} - iterate = { - if let middleware = middlewares.first { - middlewares.removeFirst() - let nextResponder = WSNextResponder(next: iterate) - _ = try middleware.respond(to: request, chainingTo: nextResponder) - } else { - try success() - } - } - try iterate() - } - } catch { - webSocket.close(code: .policyViolation) - } - } -} diff --git a/Sources/WS/WS/WS.swift b/Sources/WS/WS/WS.swift deleted file mode 100644 index 7070d18..0000000 --- a/Sources/WS/WS/WS.swift +++ /dev/null @@ -1,86 +0,0 @@ -import Vapor -import WebSocket -import Foundation - -open class WS: Service, WebSocketServer { - var server = NIOWebSocketServer.default() - var middlewares: [Middleware] = [] - - public internal(set) var clients = Set() - var clientsCache: [String: WSClient] = [:] - var channels = Set() - - var delegate: WSControllerable? - - public var logger = WSLogger(.off) - public var dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .formatted(WSDefaultDateFormatter()) - - // MARK: - Initialization - - public init(at path: [PathComponent], protectedBy middlewares: [Middleware]? = nil, delegate: WSControllerable? = nil) { - self.middlewares = middlewares ?? [] - self.delegate = delegate - self.delegate?.logger = self - server.get(at: path, use: handleConnection) - } - - public convenience init(at path: PathComponent..., protectedBy middlewares: [Middleware]? = nil, delegate: WSControllerable? = nil) { - self.init(at: path, protectedBy: middlewares, delegate: delegate) - } - - public convenience init(at path: PathComponentsRepresentable..., protectedBy middlewares: [Middleware]? = nil, delegate: WSControllerable? = nil) { - self.init(at: path.convertToPathComponents(), protectedBy: middlewares, delegate: delegate) - } - - //MARK: - - - func insertClient(_ client: WSClient) -> Bool { - if clients.insert(client).inserted { - return true - } - client.connection.close(code: .unexpectedServerError) - return false - } - - func removeClient(_ client: WSClient) { - clients.remove(client) - for (key, value) in clientsCache { - if value.cid == client.cid { - clientsCache.removeValue(forKey: key) - break - } - } - channels.forEach { $0.clients.remove(client) } - } - - //MARK: - Connection Handler - - func handleConnection(_ ws: WebSocket, _ req: Request) { - let client = WSClient(ws, req, ws: self) - delegate?.wsOnOpen(self, client) - logger.log(.info("onOpen"), - .debug("onOpen cid: " + client.cid.uuidString + "headers: \(req.http.headers)"), on: req) - ws.onText { [weak self] ws, text in - guard let self = self else { return } - self.logger.log(.info("onText"), - .debug("onText: " + text), on: req) - self.delegate?.wsOnText(self, client, text) - } - ws.onBinary { [weak self] ws, data in - guard let self = self else { return } - self.logger.log(.info("onBinary"), - .debug("onBinary: \(data.count) bytes"), on: req) - self.delegate?.wsOnBinary(self, client, data) - } - ws.onClose.always { - self.logger.log(.info("onClose"), - .debug("onClose cid: " + client.cid.uuidString), on: req) - self.delegate?.wsOnClose(self, client) - } - ws.onError { [weak self] ws, error in - guard let self = self else { return } - self.logger.log(.error("onError: \(error)"), on: req) - self.delegate?.wsOnError(self, client, error) - } - } -} diff --git a/Sources/WS/WSError.swift b/Sources/WS/WSError.swift deleted file mode 100644 index 5bba001..0000000 --- a/Sources/WS/WSError.swift +++ /dev/null @@ -1,7 +0,0 @@ -protocol WSErrorProtocol: Error {} -public struct WSError: WSErrorProtocol { - public var reason: String - init(reason: String) { - self.reason = reason - } -} diff --git a/Sources/WS/WSEvent.swift b/Sources/WS/WSEvent.swift deleted file mode 100644 index aef1c65..0000000 --- a/Sources/WS/WSEvent.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation - -public struct NoPayload: Codable {} - -public struct WSEvent: Codable { - public let event: String - public let payload: P? - public init (event: String, payload: P? = nil) { - self.event = event - self.payload = payload - } -} - -public struct WSEventPrototype: Codable { - public var event: String -} - -public struct WSOutgoingEvent: Codable { - var event: String - var payload: P? - init(_ event: String, payload: P?) { - self.event = event - self.payload = payload - } -} diff --git a/Sources/WS/WSEventIdentifier.swift b/Sources/WS/WSEventIdentifier.swift deleted file mode 100644 index 3bbbb1a..0000000 --- a/Sources/WS/WSEventIdentifier.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Foundation - -public struct WSEventIdentifier: Equatable, Hashable, CustomStringConvertible, ExpressibleByStringLiteral, Codable { - /// The unique id. - public let uid: String - - /// See `CustomStringConvertible`. - public var description: String { - return uid - } - - /// Create a new `EventIdentifier`. - public init(_ uid: String) { - self.uid = uid - } - - /// See `ExpressibleByStringLiteral`. - public init(stringLiteral value: String) { - self.init(value) - } -} - -//MARK: Local protocol events - -public struct JoinPayload: Codable { - public var channel: UUID -} - -public struct LeavePayload: Codable { - public var channel: UUID -} - -extension WSEventIdentifier { - public static var join: WSEventIdentifier { return .init("join") } - public static var leave: WSEventIdentifier { return .init("leave") } -} diff --git a/Sources/WS/WSLogger.swift b/Sources/WS/WSLogger.swift deleted file mode 100644 index 2d4bb70..0000000 --- a/Sources/WS/WSLogger.swift +++ /dev/null @@ -1,93 +0,0 @@ -import Foundation -import Vapor - -public protocol WSLoggerDelegate: class { - func onAny(_ level: WSLogger.Level, _ message: String, container: Container) - func onCurrentLevel(_ message: String, container: Container) -} - -public struct WSLogger { - public enum Level: Int { - ///Don't log anything at all. - case off - - ///An error is a serious issue and represents the failure of something important going on in your application. - ///This will require someone's attention probably sooner than later, but the application can limp along. - case error - - ///Finally, we can dial down the stress level. - ///INFO messages correspond to normal application behavior and milestones. - ///You probably won't care too much about these entries during normal operations, but they provide the skeleton of what happened. - ///A service started or stopped. You added a new user to the database. That sort of thing. - case info - - ///Here, you're probably getting into "noisy" territory and furnishing more information than you'd want in normal production situations. - case debug - } - - public enum Message { - case off - case error(String) - case info(String) - case debug(String) - - var rawValue: Int { - switch self { - case .off: return 0 - case .error: return 1 - case .info: return 2 - case .debug: return 3 - } - } - - var level: Level { - switch self { - case .off: return .off - case .error: return .error - case .info: return .info - case .debug: return .debug - } - } - - var message: String { - switch self { - case .off: return "" - case .error(let v): return v - case .info(let v): return v - case .debug(let v): return v - } - } - - var symbol: String { - switch self { - case .off: return "" - case .error: return "❗️" - case .info: return "🔔" - case .debug: return "❕" - } - } - } - - public var level: Level - public weak var delegate: WSLoggerDelegate? - - public init (_ level: Level) { - self.level = level - } - - public func log(_ message: Message..., on container: Container) { - log(message, on: container) - } - - func log(_ messages: [Message], on container: Container) { - let sorted = messages.sorted(by: { $0.rawValue < $1.rawValue }) - if let last = sorted.last { - delegate?.onAny(last.level, last.message, container: container) - } - if let last = sorted.filter({ $0.rawValue <= self.level.rawValue }).last { - let message = "⚡️ [WS][" + last.symbol + "]: " + last.message - print(message) - delegate?.onCurrentLevel(message, container: container) - } - } -} diff --git a/Sources/WS/WSNextResponder.swift b/Sources/WS/WSNextResponder.swift deleted file mode 100644 index 97540ea..0000000 --- a/Sources/WS/WSNextResponder.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation -import Vapor - -struct WSNextResponder: Responder { - typealias NextCallback = () throws -> () - - let next: NextCallback - - init(next: @escaping NextCallback) { - self.next = next - } - - func respond(to req: Request) throws -> Future { - try next() - let resp = Response(http: HTTPResponse(status: .ok, - version: req.http.version, - headers: req.http.headers, - body: req.http.body), using: req) - return req.eventLoop.newSucceededFuture(result: resp) - } -} diff --git a/Sources/WS/WSObserver.swift b/Sources/WS/WSObserver.swift deleted file mode 100644 index 3c2674c..0000000 --- a/Sources/WS/WSObserver.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation - -open class WSObserver: WSControllerable { - public weak var logger: WSLoggable? - - public init () {} - - //MARK: WSDelegate - - public func wsOnOpen(_ ws: WS, _ client: WSClient) -> Bool { - return ws.insertClient(client) - } - - public func wsOnClose(_ ws: WS, _ client: WSClient) { - ws.removeClient(client) - } - - public func wsOnText(_ ws: WS, _ client: WSClient, _ text: String) {} - public func wsOnBinary(_ ws: WS, _ client: WSClient, _ data: Data) {} - public func wsOnError(_ ws: WS, _ client: WSClient, _ error: Error) {} -}