class IntervalResponse::Sequence

Represents a linear sequence of non-overlapping, joined intervals. For example, an HTTP response which consists of multiple edge included segments, or a timeline with clips joined together. Every interval contains a segment - an arbitrary object which responds to `#size` at time of adding to the IntervalSequence.

Constants

Interval

Attributes

size[R]

@return [Integer] the sum of sizes of all the segments of the sequence

Public Class Methods

new(*segments) click to toggle source

Creates a new Sequence with given segments.

@param size or bytesize

# File lib/interval_response/sequence.rb, line 18
def initialize(*segments)
  @intervals = []
  @size = 0
  segments.each { |s| self << s }
end

Public Instance Methods

<<(segment) click to toggle source

Adds a segment to the sequence. The segment gets added at the end of the sequence.

@param segment Segment which responds to size or bytesize @return self

# File lib/interval_response/sequence.rb, line 28
def <<(segment)
  segment_size_or_bytesize = segment.respond_to?(:bytesize) ? segment.bytesize : segment.size
  add_segment(segment, size: segment_size_or_bytesize)
end
add_segment(segment, size:, etag: size) click to toggle source

Adds a segment to the sequence with specifying the size and optionally the ETag value of the segment. ETag defaults to the size of the segment. Segment can be any object as the size gets passed as a keyword argument

@param segment Any object can be used as the segment @param size The size of the segment @param etag An object that defines the ETag for the segment. Can be any object that can

be Marshal.dump - ed.

@return self

# File lib/interval_response/sequence.rb, line 42
def add_segment(segment, size:, etag: size)
  if size > 0
    etag_quoted = '"%s"' % etag
    # We save the index of the interval inside the Struct so that we can
    # use `bsearch` later instead of requiring `bsearch_index` to be available
    @intervals << Interval.new(segment, size, @size, @intervals.length, etag_quoted)
    @size += size
  end
  self
end
each_in_range(from_range_in_resource) { |sub_segment, sub_range, is_first_interval && is_first_nested_interval| ... } click to toggle source

Yields every segment which is touched by the given Range in resource in sequence, together with a Range object which defines the necessary part of the segment. For example, calling `each_in_range(0..2)` with 2 segments of size 1 and 2 will successively yield [segment1, 0..0] then [segment2, 0..1]

Interval sequences can be nested - you can place a Sequence inside another Sequence as a segment. In that case when you call `each_in_range` on the outer Sequence and you need to retrieve data from the inner Sequence which is one of the segments, the call will yield the segments from the inner Sequence, “drilling down” as deep as is appropriate.

Three arguments will be yielded to the block - the segment (the “meat” of an interval, which is the object given when the interval was added to the Sequence), the range within the interval (which is always going to be an inclusive `Range` of integers) and a boolean flag indicating whether this interval is the very first interval in the requested subset of the sequence. This flag honors nesting (if you have arbitrarily nested interval Sequences and you request something from the first interval of several Sequences deep it will still indicate `true`).

@param from_range_in_resource an inclusive Range that specifies the range within the segment map @yield segment, range_in_segment, is_first_interval

# File lib/interval_response/sequence.rb, line 72
def each_in_range(from_range_in_resource)
  # Skip empty ranges
  requested_range_size = (from_range_in_resource.end - from_range_in_resource.begin) + 1
  return if requested_range_size < 1

  # Then walk through included intervals. If the range misses
  # our intervals completely included_intervals will be empty.
  included_intervals = intervals_within_range(from_range_in_resource)
  included_intervals.each do |interval|
    int_start = interval.offset
    int_end = interval.offset + interval.size - 1
    req_start = from_range_in_resource.begin
    req_end = from_range_in_resource.end
    range_within_interval = (max(int_start, req_start) - int_start)..(min(int_end, req_end) - int_start)
    is_first_interval = interval.position == 0

    # Allow Sequences to be composed together
    if interval.segment.respond_to?(:each_in_range)
      interval.segment.each_in_range(range_within_interval) do |sub_segment, sub_range, is_first_nested_interval|
        yield(sub_segment, sub_range, is_first_interval && is_first_nested_interval)
      end
    else
      yield(interval.segment, range_within_interval, is_first_interval)
    end
  end
end
empty?() click to toggle source

Tells whether the size of the entire sequence is 0

# File lib/interval_response/sequence.rb, line 100
def empty?
  @size == 0
end
etag() click to toggle source

For IE resumes to work, a strong ETag must be set in the response, and a strong comparison must be performed on it.

ETags have meaning with Range: requests, because when a client requests a range it will send the ETag back in the If-Range header. That header tells the server that “I want to have the ranges as emitted by the response representation that has output this etag”. This is done so that there is a guarantee that the same resource being requested has the same resource length (off of which the ranges get computed), and the ranges can be safely combined by the client. In practice this means that the ETag must contain some “version handle” which stays unchanged as long as the code responsible for generating the response does not change. In our case the response can change due to the following things:

  • The lengths of the segments change

  • The contents of the segments changes

  • Code that outputs the ranges themselves changes, and outputs different offsets of differently-sized resources. A resource can be differently sized since the MIME multiplart-byte-range response can have its boundary or per-part headers change, which affects the size of the MIME part headers. Even though the boundary is not a part of the resource itself, the sizes of the part headers do contribute to the envelope size - that should stay the same as long as the ETag holds.

It is important that the returned ETag is a strong ETag (not prefixed with 'W/') and must be enclosed in double-quotes.

See for more blogs.msdn.microsoft.com/ieinternals/2011/06/03/download-resumption-in-internet-explorer/

The ETag value gets derived from the ETags of the segments, which will be Marshal.dump'ed together and then added to the hash digest to produce the final ETag value.

@return [String] a string delimited with double-quotes

# File lib/interval_response/sequence.rb, line 135
def etag
  d = Digest::SHA1.new
  d << IntervalResponse::VERSION
  @intervals.each do |interval|
    d << interval.etag
  end
  '"%s"' % d.hexdigest
end
first_interval_only?(*ranges) click to toggle source

Tells whether all of the given `ranges` will be satisfied from the first interval only. This can be used to redirect to the resource at that interval instead of proxying it through, since the `Range` header won't need to be adjusted

# File lib/interval_response/sequence.rb, line 147
def first_interval_only?(*ranges)
  ranges.map do |range|
    each_in_range(range) do |_, _, is_first_interval|
      return false unless is_first_interval
    end
  end

  true
end

Private Instance Methods

interval_under(offset) click to toggle source
# File lib/interval_response/sequence.rb, line 167
def interval_under(offset)
  # For our purposes we would be better served by `bsearch_index`, but it is not available
  # on older Ruby versions which we otherwise can splendidly support. Since when we retrieve
  # the interval under offset we are going to need the index anyway, and since calling `Array#index`
  # will incur another linear scan of the array, we save the index of the interval with the interval itself.
  @intervals.bsearch do |interval|
    # bsearch expects a 0 return value for "exact match".
    # -1 tells it "look to my left" and 1 "look to my right",
    # which is the output of the <=> operator. If we only needed
    # to find the exact offset in a sorted list just <=> would be
    # fine, but since we are looking for offsets within intervals
    # we will expand the the "match" case with "falls within interval".
    if offset >= interval.offset && offset < (interval.offset + interval.size)
      0
    else
      offset <=> interval.offset
    end
  end
end
intervals_within_range(http_range) click to toggle source
# File lib/interval_response/sequence.rb, line 187
def intervals_within_range(http_range)
  first_touched = interval_under(http_range.begin)

  # The range starts to the right of available range
  return [] unless first_touched

  last_touched = interval_under(http_range.end) || @intervals.last
  @intervals[first_touched.position..last_touched.position]
end
max(a, b) click to toggle source
# File lib/interval_response/sequence.rb, line 159
def max(a, b)
  a > b ? a : b
end
min(a, b) click to toggle source
# File lib/interval_response/sequence.rb, line 163
def min(a, b)
  a < b ? a : b
end