Wednesday, July 15, 2026 2:09:42 AM
> promise.rb
# frozen_string_literal: true

module ESM
  module Arma
    #
    # A pending response to a request sent over the wire. Registered in the {Ledger}
    # under the request's id; the read loop calls {#set_response} when the matching
    # response arrives. Callers either block on {#wait_for_response} or attach a
    # non-blocking callback with {#on_response}.
    #
    # Backed by a single +Concurrent::IVar+, which natively models the
    # fulfilled-or-rejected obligation (value/reason/fulfilled?/rejected?) that
    # callers inspect, supports a blocking +#value+ with timeout, and notifies
    # observers on resolution.
    #
    class Promise
      def initialize
        @ivar = Concurrent::IVar.new
        @timeout_task = nil
      end

      #
      # Resolves the pending response. Called by the read loop when the matching
      # response arrives. A StandardError rejects the obligation; anything else is
      # treated as a Request and fulfills it with the request's content.
      #
      # @param response [ESM::Arma::Request, StandardError] the inbound response
      #
      # @return [Boolean] false if the obligation was already resolved (a late
      #   response after a timeout), true otherwise
      #
      def set_response(response)
        if response.is_a?(StandardError)
          @ivar.fail(response)
        else
          @ivar.set(response.content)
        end

        true
      rescue Concurrent::MultipleAssignmentError
        # Already resolved (e.g. timed out a hair before the response landed).
        # Drop the late response rather than crash the read loop.
        false
      end

      #
      # Blocks the calling thread until the response resolves or the timeout
      # elapses, then returns the obligation.
      #
      # @param timeout [Numeric, nil] seconds to wait; nil waits indefinitely
      #
      # @return [Concurrent::IVar] the resolved obligation. Responds to
      #   #fulfilled?/#rejected?/#value/#reason. A timeout is surfaced as a
      #   rejection with ESM::Exception::RequestTimeout.
      #
      def wait_for_response(timeout = nil)
        @ivar.wait(timeout)
        reject_as_timeout unless @ivar.complete?
        @ivar
      end

      #
      # Registers a non-blocking callback fired with the resolved obligation when
      # the response arrives (or the timeout elapses). The calling thread is not
      # held.
      #
      # @param timeout [Numeric] seconds before the obligation is rejected as timed out
      #
      # @return [self]
      #
      def on_response(timeout, &block)
        @timeout_task = Concurrent::ScheduledTask.execute(timeout) { reject_as_timeout }

        @ivar.add_observer do |_time, _value, _reason|
          @timeout_task&.cancel
          block.call(@ivar)
        end

        self
      end

      private

      def reject_as_timeout
        @ivar.fail(ESM::Exception::RequestTimeout.new)
      rescue Concurrent::MultipleAssignmentError
        # A real response won the race; keep its result.
        nil
      end
    end
  end
end
All opinions represented herein are my own
- © 2024 - 2026 itsthedevman
- build 43aa0d7