Class: MaquinaStream::Manifest

Inherits:
Object
  • Object
show all
Defined in:
lib/maquina_stream/manifest.rb

Overview

What the browser is told a message currently is, in a bounded number of bytes.

{ seq: 412, cutoff: 38, rollup: "7c1f…",
  blocks: [["ms-m8f21-b38", "a91c…"], ["ms-m8f21-b39", "4fe2…"]] }

Not HTML. The client diffs this against its own DOM, asks for the blocks whose digests differ, and morphs only those — so repair costs what has drifted rather than what the message weighs.

Why it is windowed

docs/design.md said the manifest is "a few hundred bytes regardless of message size". Measured, listing every sealed block gave 1.8KB for a 2KB message and 88KB for a 100KB one — it tracked length almost exactly, because block count does. A keyframe every four seconds carrying 88KB is the bandwidth problem the manifest was introduced to prevent.

So the manifest carries the last window sealed blocks in full, plus one rollup digest covering everything older. A client whose rollup matches knows its history is intact and only has to consider the window; a client whose rollup differs asks for the whole thing, which is rare and is what a cold page load does anyway.

Payload is then bounded by the window, not by the message.

Wire format

Key Meaning
seq the sequence number this manifest describes
cutoff how many sealed blocks fall behind the window
rollup one digest covering every block behind the cutoff
blocks [[id, digest], …] for the blocks inside the window

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(seq:, blocks:, window: 50) ⇒ Manifest

Builds a manifest over a list of sealed blocks. .for is the usual entry point.



77
78
79
80
81
# File 'lib/maquina_stream/manifest.rb', line 77

def initialize(seq:, blocks:, window: 50)
  @seq = seq
  @blocks = blocks
  @window = window
end

Instance Attribute Details

#blocks ⇒ Object (readonly)

Every sealed Block, in order — including the ones behind the window, which #rollup covers and #windowed_entries omits.



48
49
50
# File 'lib/maquina_stream/manifest.rb', line 48

def blocks
  @blocks
end

#seq ⇒ Object (readonly)

The sequence number this manifest describes.



44
45
46
# File 'lib/maquina_stream/manifest.rb', line 44

def seq
  @seq
end

#window ⇒ Object (readonly)

How many sealed blocks are listed in full. Everything older is covered by #rollup. Float::INFINITY when the manifest was built with full: true.



52
53
54
# File 'lib/maquina_stream/manifest.rb', line 52

def window
  @window
end

Class Method Details

.for(record, config: MaquinaStream.config, window: nil, full: false) ⇒ Object

Builds the manifest for a record, over its current buffer.

full: true drops the window and lists every sealed block — what a client asks for when its #rollup disagrees with ours and the windowed diff is therefore not enough. Rare, and no more expensive than the cold page load it resembles.



60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/maquina_stream/manifest.rb', line 60

def self.for(record, config: MaquinaStream.config, window: nil, full: false)
  document = Document.new(
    record.maquina_stream_buffer,
    config: config,
    sid: record.maquina_stream_id,
    mode: record.maquina_stream_open? ? :streaming : :static
  )

  new(
    seq: record.maquina_stream_sequence,
    blocks: document.sealed_blocks,
    window: full ? Float::INFINITY : (window || config.manifest_window)
  )
end

Instance Method Details

#cutoff ⇒ Object

How many entries fall behind the window and are covered by #rollup instead of being listed. Zero for a full manifest.



90
91
92
93
94
# File 'lib/maquina_stream/manifest.rb', line 90

def cutoff
  return 0 if window.infinite?

  [entries.length - window, 0].max
end

#diff(client_digests) ⇒ Object

Which of the client's blocks disagree with ours, within the window.

Blocks the client has and we do not are not reported: the server is the authority on what exists, and a stale block is removed by the morph rather than by a separate instruction.



123
124
125
126
127
128
129
# File 'lib/maquina_stream/manifest.rb', line 123

def diff(client_digests)
  client = client_digests.to_h

  windowed_entries.filter_map do |id, digest|
    id unless client[id] == digest
  end
end

#entries ⇒ Object

Every sealed block, id and digest. The wire format sends a slice of this.



84
85
86
# File 'lib/maquina_stream/manifest.rb', line 84

def entries
  blocks.map(&:to_manifest_entry)
end

#rollup ⇒ Object

One digest covering every block older than the window. Order matters: two clients holding the same blocks in a different order are not in the same state.



104
105
106
# File 'lib/maquina_stream/manifest.rb', line 104

def rollup
  Digest::SHA256.hexdigest(entries.take(cutoff).flatten.join(" "))[0, 16]
end

#stale_history?(client_rollup) ⇒ Boolean

True when the client's view of the history behind the window disagrees with ours, and the windowed diff is therefore not enough.

Returns:

  • (Boolean)


133
134
135
# File 'lib/maquina_stream/manifest.rb', line 133

def stale_history?(client_rollup)
  cutoff.positive? && client_rollup != rollup
end

#to_h ⇒ Object

The wire format, as a Hash. What the manifest endpoint renders.



109
110
111
# File 'lib/maquina_stream/manifest.rb', line 109

def to_h
  {seq: seq, cutoff: cutoff, rollup: rollup, blocks: windowed_entries}
end

#to_json(*args) ⇒ Object

The wire format, as JSON.



114
115
116
# File 'lib/maquina_stream/manifest.rb', line 114

def to_json(*args)
  to_h.to_json(*args)
end

#windowed_entries ⇒ Object

The entries actually sent: everything from #cutoff onwards.



97
98
99
# File 'lib/maquina_stream/manifest.rb', line 97

def windowed_entries
  entries.drop(cutoff)
end