Class: Philiprehberger::WebhookBuilder::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/philiprehberger/webhook_builder/client.rb

Overview

Webhook delivery client with HMAC signing, retry, and tracking.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(url:, secret:, timeout: 30, max_retries: 3, backoff: :exponential, concurrency: 4, default_headers: {}) ⇒ Client

Create a new webhook client.

Parameters:

  • url (String)

    the webhook endpoint URL

  • secret (String)

    the HMAC-SHA256 signing secret

  • timeout (Integer) (defaults to: 30)

    HTTP timeout in seconds (default: 30)

  • max_retries (Integer) (defaults to: 3)

    maximum retry attempts on failure (default: 3)

  • backoff (Symbol, Proc) (defaults to: :exponential)

    backoff strategy — :exponential (default), :linear, :fixed, or a Proc

  • concurrency (Integer) (defaults to: 4)

    maximum concurrent threads for batch delivery (default: 4)

  • default_headers (Hash) (defaults to: {})

    headers to include in every delivery



37
38
39
40
41
42
43
44
45
46
# File 'lib/philiprehberger/webhook_builder/client.rb', line 37

def initialize(url:, secret:, timeout: 30, max_retries: 3, backoff: :exponential, concurrency: 4,
               default_headers: {})
  @url = url
  @secret = secret
  @timeout = timeout
  @max_retries = max_retries
  @backoff_strategy = Backoff.resolve(backoff)
  @concurrency = concurrency
  @default_headers = default_headers.dup.freeze
end

Instance Attribute Details

#concurrencyInteger (readonly)

Returns the maximum number of concurrent batch deliveries.

Returns:

  • (Integer)

    the maximum number of concurrent batch deliveries



23
24
25
# File 'lib/philiprehberger/webhook_builder/client.rb', line 23

def concurrency
  @concurrency
end

#default_headersHash (readonly)

Returns default headers included in every delivery.

Returns:

  • (Hash)

    default headers included in every delivery



26
27
28
# File 'lib/philiprehberger/webhook_builder/client.rb', line 26

def default_headers
  @default_headers
end

#max_retriesInteger (readonly)

Returns the maximum number of delivery attempts.

Returns:

  • (Integer)

    the maximum number of delivery attempts



20
21
22
# File 'lib/philiprehberger/webhook_builder/client.rb', line 20

def max_retries
  @max_retries
end

#timeoutInteger (readonly)

Returns the HTTP timeout in seconds.

Returns:

  • (Integer)

    the HTTP timeout in seconds



17
18
19
# File 'lib/philiprehberger/webhook_builder/client.rb', line 17

def timeout
  @timeout
end

#urlString (readonly)

Returns the webhook endpoint URL.

Returns:

  • (String)

    the webhook endpoint URL



14
15
16
# File 'lib/philiprehberger/webhook_builder/client.rb', line 14

def url
  @url
end

Instance Method Details

#deliver(event:, payload:, headers: {}) ⇒ Delivery

Deliver a webhook event.

Parameters:

  • event (String)

    the event type (e.g., “order.created”)

  • payload (Hash)

    the event payload

  • headers (Hash) (defaults to: {})

    per-delivery headers (override default_headers)

Returns:



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/philiprehberger/webhook_builder/client.rb', line 54

def deliver(event:, payload:, headers: {})
  body = JSON.generate({ event: event, payload: payload, timestamp: Time.now.utc.iso8601 })
  signature = sign(body)
  merged_headers = @default_headers.merge(headers)

  attempts = 0
  start_time = monotonic_now
  last_response_code = nil
  last_response_body = nil
  last_error = nil

  loop do
    attempts += 1
    begin
      response = send_request(body, signature, event, merged_headers)
      last_response_code = response.code.to_i
      last_response_body = response.body

      if last_response_code >= 200 && last_response_code < 300
        return Delivery.new(
          success: true,
          response_code: last_response_code,
          attempts: attempts,
          duration: monotonic_now - start_time,
          response_body: last_response_body
        )
      end

      last_error = "HTTP #{last_response_code}"
    rescue StandardError => e
      last_error = e.message
    end

    break if attempts > @max_retries

    sleep(@backoff_strategy.call(attempts))
  end

  Delivery.new(
    success: false,
    response_code: last_response_code,
    attempts: attempts,
    duration: monotonic_now - start_time,
    response_body: last_response_body,
    error: last_error
  )
end

#deliver_batch(events) ⇒ Array<Delivery>

Deliver multiple webhook events concurrently.

Parameters:

  • events (Array<Hash>)

    array of { event:, payload: } hashes, optionally with headers:

Returns:

  • (Array<Delivery>)

    delivery results in the same order as input



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/philiprehberger/webhook_builder/client.rb', line 106

def deliver_batch(events)
  results = Array.new(events.length)
  mutex = Mutex.new
  queue = Queue.new

  events.each_with_index do |item, index|
    queue << [item, index]
  end

  threads = Array.new([@concurrency, events.length].min) do
    Thread.new do
      loop do
        pair = begin
          queue.pop(true)
        rescue ThreadError
          nil
        end
        break unless pair

        item, index = pair
        delivery = deliver(
          event: item[:event],
          payload: item[:payload],
          headers: item.fetch(:headers, {})
        )
        mutex.synchronize { results[index] = delivery }
      end
    end
  end

  threads.each(&:join)
  results
end