Class: IntervalResponse::Sequence

Inherits:
Object
  • Object
show all
Defined in:
lib/interval_response/sequence.rb

Overview

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.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*segments) ⇒ Sequence

Creates a new Sequence with given segments.

Parameters:

  • segments (Array<#size,#bytesize>)

    Segments which respond to #size or #bytesize



18
19
20
21
22
# File 'lib/interval_response/sequence.rb', line 18

def initialize(*segments)
  @intervals = []
  @size = 0
  segments.each { |s| self << s }
end

Instance Attribute Details

#sizeInteger (readonly)

Returns the sum of sizes of all the segments of the sequence.

Returns:

  • (Integer)

    the sum of sizes of all the segments of the sequence



13
14
15
# File 'lib/interval_response/sequence.rb', line 13

def size
  @size
end

Instance Method Details

#<<(segment) ⇒ Object

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

Parameters:

  • segment (#size, #bytesize)

    Segment which responds to #size or #bytesize

Returns:

  • self



28
29
30
31
# 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) ⇒ Object

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

Parameters:

  • segment (Object)

    Any object can be used as the segment

  • size (Integer)

    The size of the segment

  • etag (Object) (defaults to: size)

    An object that defines the ETag for the segment. Can be any object that can be Marshal.dump - ed.

Returns:

  • self



42
43
44
45
46
47
48
49
50
51
# 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) ⇒ Object

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`).

Parameters:

  • from_range_in_resource (Range)

    an inclusive Range that specifies the range within the segment map



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
# 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?Boolean

Tells whether the size of the entire sequence is 0

Returns:

  • (Boolean)


100
101
102
# File 'lib/interval_response/sequence.rb', line 100

def empty?
  @size == 0
end

#etagString

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.

Returns:

  • (String)

    a string delimited with double-quotes



135
136
137
138
139
140
141
142
# 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) ⇒ Boolean

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

Returns:

  • (Boolean)


147
148
149
150
151
152
153
154
155
# 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