Class: TurboBoost::State::Manager

Inherits:
Object
  • Object
show all
Includes:
ActiveModel::Dirty
Defined in:
lib/turbo_boost/state/manager.rb

Overview

Class used to hold ephemeral state related to the rendered UI.

Examples:

  • Sidebar open/closed state

  • Tree view open/closed state

  • Accordion collapsed/expanded state

  • Customized layout / presentation

  • Applied data filters

  • Number of data rows to display etc.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(controller) ⇒ Manager

Returns a new instance of Manager.



41
42
43
44
45
46
47
48
49
50
51
52
53
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
# File 'lib/turbo_boost/state/manager.rb', line 41

def initialize(controller)
  @controller = controller

  begin
    @state = TurboBoost::State.new(cookie) # server state as stored in the cookie
  rescue => error
    Rails.logger.error "Failed to construct TurboBoost::State! #{error.message}"
    @state = TurboBoost::State.new
  end

  # State the server used to render the page last time
  cookie_state_hash = state.to_h

  # State managed by the server on the backend (redis cache etc.)
  # SEE: `TurboBoost::State::Manager.state_override_block`
  server_state_hash = {}

  # State the client expects... related to optimistic UI updates
  # i.e. Changes made on the client before making this request
  header_state_hash = {}

  # Apply server state overrides (i.e. state stored in databases like Redis, Postgres, etc...)
  if TurboBoost::Commands.config.apply_server_state_overrides
    begin
      state_override_block = self.class.state_override_block(controller)
      if state_override_block
        server_state_hash = controller.instance_eval(&state_override_block).with_indifferent_access
        server_state_hash.each { |key, val| self[key] = val }
      end
    rescue => error
      Rails.logger.error "Failed to apply `state_override_block` configured in #{controller.class.name} to TurboBoost::State! #{error.message}"
    end
  end

  # Apply client state overrides (i.e. optimistic state)
  # NOTE: Client state HTTP headers are only sent if/when state has changed on the client (only the changes are sent).
  #       This prevents race conditions (state mismatch) caused when frame and XHR requests emit immediately
  #       before the <meta id="turbo-boost"> has been updated with the latest state from the server.
  if TurboBoost::Commands.config.apply_client_state_overrides
    begin
      header_state_hash = TurboBoost::State.deserialize_base64(header).with_indifferent_access
      header_state_hash.each { |key, val| self[key] = val }
    rescue => error
      Rails.logger.error "Failed to apply client state from HTTP headers to TurboBoost::State! #{error.message}"
    end
  end

  @cookie_data = cookie_state_hash
  @header_data = header_state_hash
  @server_data = server_state_hash
rescue => error
  Rails.logger.error "Failed to construct TurboBoost::State! #{error.message}"
ensure
  @state ||= TurboBoost::State.new
end

Instance Attribute Details

#controllerObject (readonly)

Returns the value of attribute controller.



39
40
41
# File 'lib/turbo_boost/state/manager.rb', line 39

def controller
  @controller
end

Returns the value of attribute cookie_data.



39
40
41
# File 'lib/turbo_boost/state/manager.rb', line 39

def cookie_data
  @cookie_data
end

#header_dataObject (readonly)

Returns the value of attribute header_data.



39
40
41
# File 'lib/turbo_boost/state/manager.rb', line 39

def header_data
  @header_data
end

#server_dataObject (readonly)

Returns the value of attribute server_data.



39
40
41
# File 'lib/turbo_boost/state/manager.rb', line 39

def server_data
  @server_data
end

Class Method Details

.add_state_override_block(controller_name, block) ⇒ Object



25
26
27
# File 'lib/turbo_boost/state/manager.rb', line 25

def add_state_override_block(controller_name, block)
  state_override_blocks[controller_name] = block
end

.state_override_block(controller) ⇒ Object



29
30
31
32
33
# File 'lib/turbo_boost/state/manager.rb', line 29

def state_override_block(controller)
  return nil if state_override_blocks.blank?
  ancestor = controller.class.ancestors.find { |a| state_override_blocks[a.name] }
  state_override_blocks[ancestor.name]
end

.state_override_blocksObject



21
22
23
# File 'lib/turbo_boost/state/manager.rb', line 21

def state_override_blocks
  @state_overrides ||= {}
end

Instance Method Details

#[](*keys, default: nil) ⇒ Object



104
105
106
# File 'lib/turbo_boost/state/manager.rb', line 104

def [](*keys, default: nil)
  state.read(*keys, default: default)
end

#[]=(*keys, value) ⇒ Object



108
109
110
111
# File 'lib/turbo_boost/state/manager.rb', line 108

def []=(*keys, value)
  state_will_change! if value != self[*keys]
  value.nil? ? state.delete(*keys) : state.write(*keys, value)
end

#clearObject



119
120
121
122
# File 'lib/turbo_boost/state/manager.rb', line 119

def clear
  provisional_state.clear
  state.clear
end

#cookiesObject

Same implementation as ActionController::Base but with public visibility



100
101
102
# File 'lib/turbo_boost/state/manager.rb', line 100

def cookies
  controller.request.cookie_jar
end

#ordinal_payloadObject



130
131
132
133
134
135
# File 'lib/turbo_boost/state/manager.rb', line 130

def ordinal_payload
  provisional_state.clear
  state.shrink!
  state.prune! max_bytesize: TurboBoost::Commands.config.max_cookie_size
  state.ordinal_payload
end

#payloadObject



124
125
126
127
128
# File 'lib/turbo_boost/state/manager.rb', line 124

def payload
  provisional_state.clear
  state.shrink!
  state.payload
end

#provisional_stateObject Also known as: now



113
114
115
# File 'lib/turbo_boost/state/manager.rb', line 113

def provisional_state
  @provisional_state ||= TurboBoost::State::ProvisionalState.new(self)
end


137
138
139
140
141
142
143
# File 'lib/turbo_boost/state/manager.rb', line 137

def write_cookie
  return unless changed? || cookie.blank?
  cookies.signed["turbo_boost.state"] = {value: ordinal_payload, path: "/", expires: 1.day.from_now}
  changes_applied
rescue => error
  Rails.logger.error "Failed to write the TurboBoost::State cookie! #{error.message}"
end