diff --git a/Sources/SwiftHooks/Commands/Command.swift b/Sources/SwiftHooks/Commands/Command.swift index 6360b16..076cd5c 100644 --- a/Sources/SwiftHooks/Commands/Command.swift +++ b/Sources/SwiftHooks/Commands/Command.swift @@ -1,3 +1,16 @@ +import class NIO.EventLoopFuture + +public protocol AnyFuture { + func toVoidFuture() -> EventLoopFuture +} + +extension EventLoopFuture: AnyFuture { + @inlinable + public func toVoidFuture() -> EventLoopFuture { + self.map { _ in } + } +} + /// Base command public struct Command: ExecutableCommand { public let name: String @@ -5,10 +18,10 @@ public struct Command: ExecutableCommand { public let alias: [String] public let hookWhitelist: [HookID] public let permissionChecks: [CommandPermissionChecker] - public let closure: (SwiftHooks, CommandEvent) throws -> Void + public let closure: (CommandEvent) -> AnyFuture - public func invoke(on event: CommandEvent, using hooks: SwiftHooks) throws { - try closure(hooks, event) + public func invoke(on event: CommandEvent) -> EventLoopFuture { + return closure(event).toVoidFuture() } public func copyWith(name: String, group: String?, alias: [String], hookWhitelist: [HookID], permissionChecks: [CommandPermissionChecker], closure: @escaping Execute) -> Self { @@ -34,7 +47,7 @@ public struct Command: ExecutableCommand { self.alias = [] self.hookWhitelist = [] self.permissionChecks = [] - self.closure = { _, _ in } + self.closure = { e in e.eventLoop.makeSucceededFuture(()) } } public func validate() throws { } @@ -55,7 +68,7 @@ public struct Command: ExecutableCommand { alias: alias, hookWhitelist: hookWhitelist, permissionChecks: permissionChecks, - closure: { _, _, _ in }, + closure: { e, _ in e.eventLoop.makeSucceededFuture(()) }, arg: x ) } @@ -63,11 +76,8 @@ public struct Command: ExecutableCommand { fileprivate extension ExecutableCommand { func getArg(_ t: T.Type = T.self, _ index: inout Int, for arg: CommandArgument, on event: CommandEvent) throws -> T.ResolvedArgument where T: CommandArgumentConvertible { - func parse(_ s: String) throws -> T.ResolvedArgument { - if !arg.isOptional && s.isEmpty { - throw CommandError.ArgumentNotFound(name) - } - let t = try T.resolveArgument(s, on: event) + func parse(_ s: String?) throws -> T.ResolvedArgument { + let t = try T.resolveArgument(s, arg: arg, on: event) if (t as? AnyOptionalType)?.isNil ?? false { return t } @@ -78,10 +88,7 @@ fileprivate extension ExecutableCommand { let s = event.args[index...].joined(separator: " ") return try parse(s) } - guard let s = event.args[safe:index] else { - throw CommandError.ArgumentNotFound(arg.componentName) - } - return try parse(s) + return try parse(event.args[safe:index]) } } @@ -92,7 +99,7 @@ public struct OneArgCommand: ExecutableCommand where A: CommandArgumentConver public let alias: [String] public let hookWhitelist: [HookID] public let permissionChecks: [CommandPermissionChecker] - public let closure: (SwiftHooks, CommandEvent, A.ResolvedArgument) throws -> Void + public let closure: (CommandEvent, A.ResolvedArgument) -> AnyFuture public var readableArguments: String? { arg.description } @@ -115,10 +122,16 @@ public struct OneArgCommand: ExecutableCommand where A: CommandArgumentConver public func validate() throws { } - public func invoke(on event: CommandEvent, using hooks: SwiftHooks) throws { - var idx = 0 - let a = try getArg(A.self, &idx, for: arg, on: event) - try closure(hooks, event, a) + public func invoke(on event: CommandEvent) -> EventLoopFuture { + let p = event.eventLoop.makePromise(of: Void.self) + do { + var idx = 0 + let a = try getArg(A.self, &idx, for: arg, on: event) + closure(event, a).toVoidFuture().cascade(to: p) + } catch { + p.fail(error) + } + return p.futureResult } /// Add an argument to this command @@ -136,7 +149,7 @@ public struct OneArgCommand: ExecutableCommand where A: CommandArgumentConver alias: alias, hookWhitelist: hookWhitelist, permissionChecks: permissionChecks, - closure: { _, _, _, _ in }, + closure: { e, _, _ in e.eventLoop.makeSucceededFuture(()) }, argOne: arg, argTwo: GenericCommandArgument(componentType: B.typedName, componentName: name) ) @@ -150,7 +163,7 @@ public struct TwoArgCommand: ExecutableCommand where A: CommandArgumentCon public let alias: [String] public let hookWhitelist: [HookID] public let permissionChecks: [CommandPermissionChecker] - public let closure: (SwiftHooks, CommandEvent, A.ResolvedArgument, B.ResolvedArgument) throws -> Void + public let closure: (CommandEvent, A.ResolvedArgument, B.ResolvedArgument) -> AnyFuture public var readableArguments: String? { [argOne, argTwo].map(\.description).joined(separator: " ") } @@ -179,11 +192,17 @@ public struct TwoArgCommand: ExecutableCommand where A: CommandArgumentCon } } - public func invoke(on event: CommandEvent, using hooks: SwiftHooks) throws { - var idx = 0 - let a = try getArg(A.self, &idx, for: argOne, on: event) - let b = try getArg(B.self, &idx, for: argTwo, on: event) - try self.closure(hooks, event, a, b) + public func invoke(on event: CommandEvent) -> EventLoopFuture { + let p = event.eventLoop.makePromise(of: Void.self) + do { + var idx = 0 + let a = try getArg(A.self, &idx, for: argOne, on: event) + let b = try getArg(B.self, &idx, for: argTwo, on: event) + self.closure(event, a, b).toVoidFuture().cascade(to: p) + } catch { + p.fail(error) + } + return p.futureResult } /// Add an argument to this command @@ -201,7 +220,7 @@ public struct TwoArgCommand: ExecutableCommand where A: CommandArgumentCon alias: alias, hookWhitelist: hookWhitelist, permissionChecks: permissionChecks, - closure: { _, _, _, _, _ in }, + closure: { e, _, _, _ in e.eventLoop.makeSucceededFuture(()) }, argOne: argOne, argTwo: argTwo, argThree: GenericCommandArgument(componentType: C.typedName, componentName: name) @@ -216,7 +235,7 @@ public struct ThreeArgCommand: ExecutableCommand where A: CommandArgume public let alias: [String] public let hookWhitelist: [HookID] public let permissionChecks: [CommandPermissionChecker] - public let closure: (SwiftHooks, CommandEvent, A.ResolvedArgument, B.ResolvedArgument, C.ResolvedArgument) throws -> Void + public let closure: (CommandEvent, A.ResolvedArgument, B.ResolvedArgument, C.ResolvedArgument) -> AnyFuture public var readableArguments: String? { [argOne, argTwo, argThree].map(\.description).joined(separator: " ") } @@ -250,12 +269,18 @@ public struct ThreeArgCommand: ExecutableCommand where A: CommandArgume } } - public func invoke(on event: CommandEvent, using hooks: SwiftHooks) throws { - var idx = 0 - let a = try getArg(A.self, &idx, for: argOne, on: event) - let b = try getArg(B.self, &idx, for: argTwo, on: event) - let c = try getArg(C.self, &idx, for: argThree, on: event) - try self.closure(hooks, event, a, b, c) + public func invoke(on event: CommandEvent) -> EventLoopFuture { + let p = event.eventLoop.makePromise(of: Void.self) + do { + var idx = 0 + let a = try getArg(A.self, &idx, for: argOne, on: event) + let b = try getArg(B.self, &idx, for: argTwo, on: event) + let c = try getArg(C.self, &idx, for: argThree, on: event) + self.closure(event, a, b, c).toVoidFuture().cascade(to: p) + } catch { + p.fail(error) + } + return p.futureResult } /// Add an argument to this command @@ -273,7 +298,7 @@ public struct ThreeArgCommand: ExecutableCommand where A: CommandArgume alias: alias, hookWhitelist: hookWhitelist, permissionChecks: permissionChecks, - closure: { _, _, _ in }, + closure: { e, _ in e.eventLoop.makeSucceededFuture(()) }, arguments: [argOne, argTwo, argThree, GenericCommandArgument(componentType: T.typedName, componentName: name)] ) } @@ -286,7 +311,7 @@ public struct ArrayArgCommand: ExecutableCommand { public let alias: [String] public let hookWhitelist: [HookID] public let permissionChecks: [CommandPermissionChecker] - public let closure: (SwiftHooks, CommandEvent, Arguments) throws -> Void + public let closure: (CommandEvent, Arguments) throws -> AnyFuture /// Arguments for this command. public let arguments: [CommandArgument] @@ -316,8 +341,14 @@ public struct ArrayArgCommand: ExecutableCommand { } } - public func invoke(on event: CommandEvent, using hooks: SwiftHooks) throws { - try closure(hooks, event, Arguments(arguments)) + public func invoke(on event: CommandEvent) -> EventLoopFuture { + let p = event.eventLoop.makePromise(of: Void.self) + do { + try closure(event, Arguments(arguments)).toVoidFuture().cascade(to: p) + } catch { + p.fail(error) + } + return p.futureResult } /// Add an argument to this command @@ -335,7 +366,7 @@ public struct ArrayArgCommand: ExecutableCommand { alias: alias, hookWhitelist: hookWhitelist, permissionChecks: permissionChecks, - closure: { _, _, _ in }, + closure: { e, _ in e.eventLoop.makeSucceededFuture(()) }, arguments: arguments + GenericCommandArgument(componentType: T.typedName, componentName: name) ) } diff --git a/Sources/SwiftHooks/Commands/Commands.swift b/Sources/SwiftHooks/Commands/Commands.swift index f8d3f6d..8a2cd87 100644 --- a/Sources/SwiftHooks/Commands/Commands.swift +++ b/Sources/SwiftHooks/Commands/Commands.swift @@ -34,7 +34,9 @@ public struct Group: Commands { return commands.executables() } - /// NOTE: Only one group prefix is supported. So calling `.group(_:)` on a Group will not do anything + /// Add a group prefix to these commands. + /// + /// **NOTE**: Only one group prefix is supported. So calling `.group(_:)` on a Group will not do anything public func group(_ group: String) -> Group { return self } diff --git a/Sources/SwiftHooks/Commands/ExecutableCommand.swift b/Sources/SwiftHooks/Commands/ExecutableCommand.swift index b0fd0c7..fefc893 100644 --- a/Sources/SwiftHooks/Commands/ExecutableCommand.swift +++ b/Sources/SwiftHooks/Commands/ExecutableCommand.swift @@ -1,3 +1,5 @@ +import class NIO.EventLoopFuture + /// Base `ExecutableCommand` public protocol _ExecutableCommand: Commands { /// Help description of the command. Used to explain usage to users. @@ -22,7 +24,7 @@ public protocol _ExecutableCommand: Commands { /// Validates if a command is valid. See also: `CommandError` func validate() throws /// Invokes a command on given event. - func invoke(on event: CommandEvent, using hooks: SwiftHooks) throws + func invoke(on event: CommandEvent) -> EventLoopFuture } /// Base `ExecutableCommand` diff --git a/Sources/SwiftHooks/Commands/SwiftHooks+Commands.swift b/Sources/SwiftHooks/Commands/SwiftHooks+Commands.swift index 566be7f..659a147 100644 --- a/Sources/SwiftHooks/Commands/SwiftHooks+Commands.swift +++ b/Sources/SwiftHooks/Commands/SwiftHooks+Commands.swift @@ -1,33 +1,54 @@ +import NIO +import struct Dispatch.DispatchTime import Logging import Metrics +extension Userable { + var prefix: String { + return mention + " " + } +} + extension SwiftHooks { - func handleMessage(_ message: Messageable, from h: _Hook) { - guard config.commands.enabled, message.content.starts(with: self.config.commands.prefix) else { return } - let foundCommands = self.findCommands(for: message) + func handleMessage(_ message: Messageable, from h: _Hook, on eventLoop: EventLoop) { + let p: String? + switch self.config.commands.prefix { + case .mention: p = h.user?.prefix + case .string(let s): p = s + } + guard let prefix = p, config.commands.enabled, message.content.starts(with: prefix) else { return } + let foundCommands = self.findCommands(for: message, withPrefix: prefix) foundCommands.forEach { (command) in guard command.hookWhitelist.isEmpty || command.hookWhitelist.contains(h.id) else { return } - let event = CommandEvent(hooks: self, cmd: command, msg: message, for: h) + let event = CommandEvent(hooks: self, cmd: command, msg: message, for: h, on: eventLoop) - do { - event.logger.debug("Invoking command") - event.logger.trace("Full message: \(message.content)") - try Timer.measure(label: "command_duration", dimensions: [("command", command.fullTrigger)]) { - try command.invoke(on: event, using: self) - } - event.logger.debug("Command succesfully invoked.") - } catch let e { - event.message.error(e, on: command) - event.logger.error("\(e.localizedDescription)") - Counter(label: "command_failure", dimensions: [("command", command.fullTrigger)]).increment() + event.logger.debug("Invoking command") + event.logger.trace("Full message: \(message.content)") + let timer = Timer(label: "command_duration", dimensions: [("command", command.fullTrigger)]) + let start = DispatchTime.now().uptimeNanoseconds + command.invoke(on: event) + .flatMapErrorThrowing({ (e) in + event.message.error(e, on: command) + throw e + }) + .whenComplete { result in + let delta = DispatchTime.now().uptimeNanoseconds - start + timer.recordNanoseconds(delta) + switch result { + case .success(_): + event.logger.debug("Command succesfully invoked.") + Counter(label: "command_finish", dimensions: [("command", command.fullTrigger), ("status", "success")]).increment() + case .failure(let e): + event.logger.error("\(e.localizedDescription)") + Counter(label: "command_finish", dimensions: [("command", command.fullTrigger), ("status", "failure")]).increment() + } } - Counter(label: "command_success", dimensions: [("command", command.fullTrigger)]).increment() } } - func findCommands(for message: Messageable) -> [_ExecutableCommand] { - return self.commands.compactMap { return message.content.starts(with: self.config.commands.prefix + $0.fullTrigger) ? $0 : nil } + func findCommands(for message: Messageable, withPrefix prefix: String) -> [_ExecutableCommand] { + return self.commands.compactMap { return message.content.starts(with: prefix + $0.fullTrigger) ? $0 : nil } } } @@ -81,6 +102,8 @@ public struct CommandEvent { public let hook: _Hook /// Command specific logger. Has command trigger set as command metadata by default. public private(set) var logger: Logger + /// EventLoop this Command runs on. + public let eventLoop: EventLoop /// Create a new `CommandEvent` /// @@ -89,7 +112,7 @@ public struct CommandEvent { /// - cmd: Command this event is wrapping. /// - msg: Message that executed the command. /// - h: `_Hook` that originally dispatched this command. - public init(hooks: SwiftHooks, cmd: _ExecutableCommand, msg: Messageable, for h: _Hook) { + public init(hooks: SwiftHooks, cmd: _ExecutableCommand, msg: Messageable, for h: _Hook, on loop: EventLoop) { self.logger = Logger(label: "SwiftHooks.Command") self.hooks = hooks self.user = msg.gAuthor @@ -103,6 +126,7 @@ public struct CommandEvent { self.name = name self.args = comps.map(String.init) self.hook = h + self.eventLoop = loop self.logger[metadataKey: "command"] = "\(cmd.fullTrigger)" } } diff --git a/Sources/SwiftHooks/Config.swift b/Sources/SwiftHooks/Config.swift index 07007f2..caa77dc 100644 --- a/Sources/SwiftHooks/Config.swift +++ b/Sources/SwiftHooks/Config.swift @@ -3,21 +3,32 @@ import Foundation /// SwiftHooks configuration. Use `.default` for default config. public struct SwiftHooksConfig { public struct Commands { - public let prefix: String + public enum Prefix { + /// Use a static String as prefix. For example `!` or `/` + case string(String) + /// Use a mention as prefix. + case mention + } + + /// Command prefix used. + public let prefix: Prefix + /// Wether or not to enable commands. public let enabled: Bool /// - parameters: - /// - prefix: Command prefix to use. Default is `!` + /// - prefix: Command prefix to use. Default is mentioning your bot. /// - enabled: Wether or not to enable commands. Default is `true` - public init(prefix: String, enabled: Bool) { + public init(prefix: Prefix, enabled: Bool) { self.prefix = prefix self.enabled = enabled } } + /// Commands config public let commands: Commands - public static let `default`: SwiftHooksConfig = .init(commands: .init(prefix: "!", enabled: true)) + /// Default config + public static let `default`: SwiftHooksConfig = .init(commands: .init(prefix: .mention, enabled: true)) /// - parameters: /// - commands: Commands configuration. diff --git a/Sources/SwiftHooks/Hooks/Hook.swift b/Sources/SwiftHooks/Hooks/Hook.swift index 2ec9372..563c772 100644 --- a/Sources/SwiftHooks/Hooks/Hook.swift +++ b/Sources/SwiftHooks/Hooks/Hook.swift @@ -36,6 +36,8 @@ public protocol _Hook { /// Refference to the main `SwiftHooks` class. var hooks: SwiftHooks? { get } + /// The bot user this Hook controls. Can be `nil` if no bot user exists. + var user: Userable? { get } /// EventLoopGroup this hook is running on. If not used standalone, this will be shared with the main `SwiftHooks` class. var eventLoopGroup: EventLoopGroup { get } /// Identifier of the hook. See `HookID` @@ -51,7 +53,7 @@ public protocol _Hook { /// Dispatch an event. /// /// This function should invoke all listeners listening for `E`, and signal the event to the main `SwiftHooks` class. - func dispatchEvent(_ event: E, with raw: Data) where E: EventType + func dispatchEvent(_ event: E, with raw: Data, on eventLoop: EventLoop) -> EventLoopFuture where E: EventType /// Used for global events. /// @@ -61,7 +63,7 @@ public protocol _Hook { /// Used for global events. /// /// Gets a concrete type for a global event, type-erased to a protocol. - func decodeConcreteType(for event: GlobalEvent, with data: Data, as t: T.Type) -> T? + func decodeConcreteType(for event: GlobalEvent, with data: Data, as t: T.Type, on eventLoop: EventLoop) -> T? } public extension _Hook { diff --git a/Sources/SwiftHooks/Listeners/EventListeners.swift b/Sources/SwiftHooks/Listeners/EventListeners.swift index fbf667d..dce62a7 100644 --- a/Sources/SwiftHooks/Listeners/EventListeners.swift +++ b/Sources/SwiftHooks/Listeners/EventListeners.swift @@ -52,6 +52,25 @@ public struct Listener: EventListeners where T: _Event, T.ContentType = self.closure = closure } + /// Create new `Listener`. + /// + /// - parameters: + /// - event: Event to listen for. + /// - closure: Closure to invoke when receiving event `T`. + public init(_ event: T, _ closure: @escaping SyncEventHandler) { + self.event = event + self.closure = { d, i in + let p = d.eventLoop.makePromise(of: Void.self) + do { + try closure(d, i) + p.succeed(()) + } catch { + p.fail(error) + } + return p.futureResult + } + } + public func register(to h: SwiftHooks) { h.listen(for: event, closure) } @@ -79,6 +98,20 @@ public struct GlobalListener: EventListeners where T: _GEvent, T.ContentTy self.closure = closure } + public init(_ event: T, _ closure: @escaping SyncEventHandler) { + self.event = event + self.closure = { d, i in + let p = d.eventLoop.makePromise(of: Void.self) + do { + try closure(d, i) + p.succeed(()) + } catch { + p.fail(error) + } + return p.futureResult + } + } + public func register(to h: SwiftHooks) { h.gListen(for: event, closure) } diff --git a/Sources/SwiftHooks/Listeners/SwiftHooks+Dispatch.swift b/Sources/SwiftHooks/Listeners/SwiftHooks+Dispatch.swift index 18c2f54..6ce0ce5 100644 --- a/Sources/SwiftHooks/Listeners/SwiftHooks+Dispatch.swift +++ b/Sources/SwiftHooks/Listeners/SwiftHooks+Dispatch.swift @@ -1,8 +1,9 @@ +import NIO import struct Foundation.Data import class Metrics.Counter -public typealias EventClosure = (Data) throws -> Void -public typealias GlobalEventClosure = (Data, _Hook) throws -> Void +public typealias EventClosure = (Data, EventLoop) -> EventLoopFuture +public typealias GlobalEventClosure = (Data, _Hook, EventLoop) -> EventLoopFuture extension SwiftHooks { /// Dispatch an event from a `_Hook` into the central `SwiftHooks` system. @@ -11,24 +12,23 @@ extension SwiftHooks { /// - e: Event to dispatch /// - raw: Raw bytes containing the event. /// - h: Hook this event originated from. - public func dispatchEvent(_ e: E, with raw: Data, from h: _Hook) where E: EventType { - guard let event = h.translate(e) else { return } + public func dispatchEvent(_ e: E, with raw: Data, from h: _Hook, on eventLoop: EventLoop) -> EventLoopFuture where E: EventType { + guard let event = h.translate(e) else { return eventLoop.makeSucceededFuture(()) } Counter(label: "global_events_dispatched", dimensions: [("hook", type(of: h).id.identifier), ("event", "\(event)")]).increment() - self.handleInternals(event, with: raw, from: h) + self.handleInternals(event, with: raw, from: h, on: eventLoop) - let handlers = self.globalListeners[event] - handlers?.forEach({ (handler) in - do { - try handler(raw, h) - } catch { - self.logger.error("\(error.localizedDescription)") - } - }) + let futures = self.lock.withLock { () -> [EventLoopFuture] in + let handlers = self.globalListeners[event] ?? [] + return handlers.map({ (handler) in + handler(raw, h, eventLoop) + }) + } + return EventLoopFuture.andAllSucceed(futures, on: eventLoop) } - private func handleInternals(_ event: GlobalEvent, with raw: Data, from h: _Hook) { - if event == ._messageCreate, let m = h.decodeConcreteType(for: event, with: raw, as: Messageable.self) { - self.handleMessage(m, from: h) + private func handleInternals(_ event: GlobalEvent, with raw: Data, from h: _Hook, on eventLoop: EventLoop) { + if event == ._messageCreate, let m = h.decodeConcreteType(for: event, with: raw, as: Messageable.self, on: eventLoop) { + self.handleMessage(m, from: h, on: eventLoop) } } } diff --git a/Sources/SwiftHooks/Listeners/SwiftHooks+Listeners.swift b/Sources/SwiftHooks/Listeners/SwiftHooks+Listeners.swift index bf8eb34..0c625a7 100644 --- a/Sources/SwiftHooks/Listeners/SwiftHooks+Listeners.swift +++ b/Sources/SwiftHooks/Listeners/SwiftHooks+Listeners.swift @@ -11,14 +11,21 @@ extension SwiftHooks { func gListen(for event: T, _ handler: @escaping EventHandler) where T: _GEvent, T.ContentType == I { guard let event = event as? _GlobalEvent else { self.logger.error("`SwiftHooks.gListen(for:_:)` called with a non `_GlobalEvent` type. This should never happen."); return } var closures = self.globalListeners[event, default: []] - closures.append { (data, hook) in - guard let object = hook.decodeConcreteType(for: event.event, with: data, as: I.self) else { + closures.append { (data, hook, el) in + guard let object = hook.decodeConcreteType(for: event.event, with: data, as: I.self, on: el) else { self.logger.debug("Unable to extract \(I.self) from data.") - return + return el.makeFailedFuture(SwiftHooksError.ConcreteTypeCreationFailure("\(I.self)")) } - try handler(.init(self.eventLoopGroup.next()), object) + return handler(.init(el), object) } self.globalListeners[event] = closures } } + + +public enum SwiftHooksError: Error { + case ConcreteTypeCreationFailure(String) + case DispatchCreationError + case GenericTypeCreationFailure(String) +} diff --git a/Sources/SwiftHooks/SwiftHooks.swift b/Sources/SwiftHooks/SwiftHooks.swift index bac3f3a..97ce77c 100644 --- a/Sources/SwiftHooks/SwiftHooks.swift +++ b/Sources/SwiftHooks/SwiftHooks.swift @@ -1,5 +1,6 @@ import Foundation import NIO +import NIOConcurrencyHelpers import Logging /// Main SwiftHooks class. Acts as global controller. @@ -26,6 +27,8 @@ public final class SwiftHooks { /// Registered `Plugin`s. public internal(set) var plugins: [_Plugin] + internal let lock: Lock + /// Global `JSONDecoder` public static let decoder = JSONDecoder() /// Global `JSONEncoder` @@ -50,6 +53,7 @@ public final class SwiftHooks { self.commands = [] self.plugins = [] self.config = config + self.lock = Lock() } /// Run the SwiftHooks process. Will boot all connected `Hook`s and block forever. diff --git a/Sources/SwiftHooks/Types/Event.swift b/Sources/SwiftHooks/Types/Event.swift index 21338e2..c7a0802 100644 --- a/Sources/SwiftHooks/Types/Event.swift +++ b/Sources/SwiftHooks/Types/Event.swift @@ -1,22 +1,23 @@ import struct Foundation.Data -import protocol NIO.EventLoop +import NIO -public typealias EventHandler = (D, I) throws -> Void +public typealias EventHandler = (D, I) -> EventLoopFuture +public typealias SyncEventHandler = (D, I) throws -> Void public protocol EventDispatch { var eventLoop: EventLoop { get } - init?(_ h: _Hook) + init?(_ h: _Hook, eventLoop: EventLoop) } public protocol EventType: Hashable {} public protocol PayloadType: Decodable { - static func create(from data: Data, on h: _Hook) -> Self? + static func create(from data: Data, on h: _Hook, on eventLoop: EventLoop) -> Self? } public extension PayloadType { - static func create(from data: Data, on _: _Hook) -> Self? { + static func create(from data: Data, on _: _Hook, on eventLoop: EventLoop) -> Self? { do { return try SwiftHooks.decoder.decode(Self.self, from: data) } catch { diff --git a/Sources/SwiftHooks/Types/GlobalEvent.swift b/Sources/SwiftHooks/Types/GlobalEvent.swift index b14e1da..ba28db0 100644 --- a/Sources/SwiftHooks/Types/GlobalEvent.swift +++ b/Sources/SwiftHooks/Types/GlobalEvent.swift @@ -26,7 +26,7 @@ public struct _GlobalEvent: _GEvent { } public struct GlobalDispatch: EventDispatch { - public init?(_ h: _Hook) { + public init?(_ h: _Hook, eventLoop: EventLoop) { return nil } diff --git a/Tests/SwiftHooksTests/SwiftHooksTests.swift b/Tests/SwiftHooksTests/SwiftHooksTests.swift index e08b63b..cbe417c 100644 --- a/Tests/SwiftHooksTests/SwiftHooksTests.swift +++ b/Tests/SwiftHooksTests/SwiftHooksTests.swift @@ -29,7 +29,7 @@ final class SwiftHooksTests: XCTestCase { try hooks.boot() - testHook.dispatchEvent(TestEvent._messageCreate, with: Data()) + try testHook.dispatchEvent(TestEvent._messageCreate, with: Data(), on: testHook.eventLoopGroup.next()).wait() XCTAssertEqual(plugin.messages.count, 2) XCTAssertEqual(plugin.messages, ["!ping", "!ping"]) diff --git a/Tests/SwiftHooksTests/TestHook.swift b/Tests/SwiftHooksTests/TestHook.swift index f7b795e..5107d2f 100644 --- a/Tests/SwiftHooksTests/TestHook.swift +++ b/Tests/SwiftHooksTests/TestHook.swift @@ -1,5 +1,6 @@ import Foundation import NIO +import NIOConcurrencyHelpers import SwiftHooks extension HookID { @@ -9,10 +10,13 @@ extension HookID { } final class TestHook: Hook { + var user: Userable? { nil } + struct _Options: HookOptions { } typealias Options = _Options var closures: [TestEvent: [EventClosure]] = [:] + let lock = Lock() func shutdown() { } @@ -38,51 +42,53 @@ final class TestHook: Hook { } } - func decodeConcreteType(for event: GlobalEvent, with data: Data, as t: T.Type) -> T? { + func decodeConcreteType(for event: GlobalEvent, with data: Data, as t: T.Type, on eventLoop: EventLoop) -> T? { switch event { - case ._messageCreate: return TestMessage.create(from: data, on: self) as? T + case ._messageCreate: return TestMessage.create(from: data, on: self, on: eventLoop) as? T } } func listen(for event: T, handler: @escaping EventHandler) where T : _Event, I == T.ContentType, T.D == D { guard let event = event as? _TestEvent else { return } var closures = self.closures[event, default: []] - closures.append { (data) in - guard let object = I.create(from: data, on: self) else { + closures.append { (data, el) in + guard let object = I.create(from: data, on: self, on: el) else { SwiftHooks.logger.debug("Unable to extract \(I.self) from data.") - return + return el.makeFailedFuture(SwiftHooksError.GenericTypeCreationFailure("\(I.self)")) } - guard let d = D.init(self) else { + guard let d = D.init(self, eventLoop: el) else { SwiftHooks.logger.debug("Unable to wrap \(I.self) in \(D.self) dispatch.") - return + return el.makeFailedFuture(SwiftHooksError.DispatchCreationError) } - try handler(d, object) + return handler(d, object) } self.closures[event] = closures } - func dispatchEvent(_ event: E, with raw: Data) where E : EventType { - defer { - self.hooks?.dispatchEvent(event, with: raw, from: self) - } - guard let event = event as? TestEvent else { return } - let handlers = self.closures[event] - handlers?.forEach({ (handler) in - do { - try handler(raw) - } catch { - SwiftHooks.logger.error("\(error.localizedDescription)") + func dispatchEvent(_ event: E, with raw: Data, on eventLoop: EventLoop) -> EventLoopFuture where E : EventType { + func unwrapAndFuture(_ x: Void = ()) -> EventLoopFuture { + if let hooks = self.hooks { + return hooks.dispatchEvent(event, with: raw, from: self, on: eventLoop) + } else { + return eventLoop.makeSucceededFuture(()) } - }) + } + guard let event = event as? TestEvent else { return unwrapAndFuture() } + let futures = self.lock.withLock { () -> [EventLoopFuture] in + let handlers = self.closures[event] ?? [] + return handlers.map({ (handler) in + return handler(raw, eventLoop) + }) + } + return EventLoopFuture.andAllSucceed(futures, on: eventLoop).flatMap(unwrapAndFuture) } } struct TestDispatch: EventDispatch { var eventLoop: EventLoop - init?(_ h: _Hook) { - guard let h = h as? TestHook else { return nil } - self.eventLoop = h.eventLoopGroup.next() + init?(_ h: _Hook, eventLoop: EventLoop) { + self.eventLoop = eventLoop } } @@ -131,7 +137,7 @@ struct TestMessage: Messageable { public var author: Userable { _author } public var _author: TestUser - static func create(from data: Data, on _: _Hook) -> TestMessage? { + static func create(from data: Data, on _: _Hook, on _: EventLoop) -> TestMessage? { return TestMessage(data) }