Repository: apple/swift-nio
File: Sources/NIOTLS/NIOTypedApplicationProtocolNegotiationHandler.swift (Swift)

Code before:
    public init(eventLoop: EventLoop, alpnCompleteHandler: @escaping (ALPNResult, Channel) -> EventLoopFuture<NIOProtocolNegotiationResult<NegotiationResult>>) {
        self.completionHandler = alpnCompleteHandler
        self.negotiatedPromise = eventLoop.makePromise(of: NIOProtocolNegotiationResult<NegotiationResult>.self)
    }

    /// Create an `ApplicationProtocolNegotiationHandler` with the given completion
    /// callback.
    ///
    /// - Parameter alpnCompleteHandler: The closure that will fire when ALPN
    ///   negotiation has completed.
    @_spi(AsyncChannel)
    public convenience init(eventLoop: EventLoop, alpnCompleteHandler: @escaping (ALPNResult) -> EventLoopFuture<NIOProtocolNegotiationResult<NegotiationResult>>) {
        self.init(eventLoop: eventLoop) { result, _ in
            alpnCompleteHandler(result)
        }
    }

    deinit {
        self.negotiatedPromise.fail(ChannelError.inappropriateOperationForState)
    }

    @_spi(AsyncChannel)
    public func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
        switch self.stateMachine.userInboundEventTriggered(event: event) {
        case .fireUserInboundEventTriggered:
            context.fireUserInboundEventTriggered(event)

        case .invokeUserClosure(let result):
            self.invokeUserClosure(context: context, result: result)
        }
    }

    @_spi(AsyncChannel)
    public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
        switch self.stateMachine.channelRead(data: data) {
        case .fireChannelRead:
            context.fireChannelRead(data)

        case .none:
            break
        }
    }

    @_spi(AsyncChannel)
    public func channelInactive(context: ChannelHandlerContext) {
        self.stateMachine.channelInactive()
        
        self.negotiatedPromise.fail(ChannelError.outputClosed)
        context.fireChannelInactive()
    }


Review comment (bug):
This solution seems wrong. We should rely on a channel lifecycle event to do this, probably `handlerRemoved`, and we should also know whether we already completed a promise or not.

Code after:
    public init(eventLoop: EventLoop, alpnCompleteHandler: @escaping (ALPNResult, Channel) -> EventLoopFuture<NIOProtocolNegotiationResult<NegotiationResult>>) {
        self.completionHandler = alpnCompleteHandler
        self.negotiatedPromise = eventLoop.makePromise(of: NIOProtocolNegotiationResult<NegotiationResult>.self)
    }

    /// Create an `ApplicationProtocolNegotiationHandler` with the given completion
    /// callback.
    ///
    /// - Parameter alpnCompleteHandler: The closure that will fire when ALPN
    ///   negotiation has completed.
    @_spi(AsyncChannel)
    public convenience init(eventLoop: EventLoop, alpnCompleteHandler: @escaping (ALPNResult) -> EventLoopFuture<NIOProtocolNegotiationResult<NegotiationResult>>) {
        self.init(eventLoop: eventLoop) { result, _ in
            alpnCompleteHandler(result)
        }
    }

    deinit {
        switch self.stateMachine.deinitHandler() {
        case .failPromise:
            self.negotiatedPromise.fail(ChannelError.inappropriateOperationForState)

        case .none:
            break
        }
    }

    @_spi(AsyncChannel)
    public func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
        switch self.stateMachine.userInboundEventTriggered(event: event) {
        case .fireUserInboundEventTriggered:
            context.fireUserInboundEventTriggered(event)

        case .invokeUserClosure(let result):
            self.invokeUserClosure(context: context, result: result)
        }
    }

    @_spi(AsyncChannel)
    public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
        switch self.stateMachine.channelRead(data: data) {
        case .fireChannelRead:
            context.fireChannelRead(data)

        case .none:
            break
        }
    }

    @_spi(AsyncChannel)
    public func channelInactive(context: ChannelHandlerContext) {