class LHC::Throttle
def after_response
def after_response options = response.request.options.dig(:throttle) return unless throttle?(options) self.class.track ||= {} self.class.track[options.dig(:provider)] = { limit: limit(options: options[:limit], response: response), remaining: remaining(options: options[:remaining], response: response), expires: expires(options: options[:expires], response: response) } end
def before_request
def before_request options = request.options.dig(:throttle) return unless options break_options = options.dig(:break) return unless break_options break_when_quota_reached! if break_options.match?('%') end
def break_when_quota_reached!
def break_when_quota_reached! options = request.options.dig(:throttle) track = (self.class.track || {}).dig(options[:provider]) return if track.blank? || track[:remaining].blank? || track[:limit].blank? || track[:expires].blank? return if Time.zone.now > track[:expires] # avoid floats by multiplying with 100 remaining = track[:remaining] * 100 limit = track[:limit] quota = 100 - options[:break].to_i raise(OutOfQuota, "Reached predefined quota for #{options[:provider]}") if remaining < quota * limit end
def convert_expires(value)
def convert_expires(value) return if value.blank? return value.call(response) if value.is_a?(Proc) return Time.parse(value) if value.match?(/GMT/) Time.zone.at(value.to_i).to_datetime end
def expires(options:, response:)
def expires(options:, response:) @expires ||= convert_expires(read_expire_option(options, response)) end
def limit(options:, response:)
def limit(options:, response:) @limit ||= if options.is_a?(Proc) options.call(response) elsif options.is_a?(Integer) options elsif options.is_a?(Hash) && options[:header] response.headers[options[:header]]&.to_i end end
def read_expire_option(options, response)
def read_expire_option(options, response) (options.is_a?(Hash) && options[:header]) ? response.headers[options[:header]] : options end
def remaining(options:, response:)
def remaining(options:, response:) @remaining ||= begin if options.is_a?(Proc) options.call(response) elsif options.is_a?(Hash) && options[:header] response.headers[options[:header]]&.to_i end end end
def throttle?(options)
def throttle?(options) [options&.dig(:track), response.headers].none?(&:blank?) end