turbo_boost-commands 0.0.18 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of turbo_boost-commands might be problematic. Click here for more details.

@@ -1,162 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../state"
4
- require_relative "provisional_state"
5
-
6
- # Class used to hold ephemeral state related to the rendered UI.
7
- #
8
- # Examples:
9
- #
10
- # - Sidebar open/closed state
11
- # - Tree view open/closed state
12
- # - Accordion collapsed/expanded state
13
- # - Customized layout / presentation
14
- # - Applied data filters
15
- # - Number of data rows to display etc.
16
- #
17
- class TurboBoost::State::Manager
18
- include ActiveModel::Dirty
19
-
20
- class << self
21
- def state_override_blocks
22
- @state_overrides ||= {}
23
- end
24
-
25
- def add_state_override_block(controller_name, block)
26
- state_override_blocks[controller_name] = block
27
- end
28
-
29
- def state_override_block(controller)
30
- return nil if state_override_blocks.blank?
31
- ancestor = controller.class.ancestors.find { |a| state_override_blocks[a.name] }
32
- state_override_blocks[ancestor.name]
33
- end
34
- end
35
-
36
- # For ActiveModel::Dirty tracking
37
- define_attribute_methods :state
38
-
39
- attr_reader :controller, :cookie_data, :header_data, :server_data
40
-
41
- def initialize(controller)
42
- @controller = controller
43
-
44
- begin
45
- @state = TurboBoost::State.new(cookie) # server state as stored in the cookie
46
- rescue => error
47
- Rails.logger.error "Failed to construct TurboBoost::State! #{error.message}"
48
- @state = TurboBoost::State.new
49
- end
50
-
51
- # State the server used to render the page last time
52
- cookie_state_hash = state.to_h
53
-
54
- # State managed by the server on the backend (redis cache etc.)
55
- # SEE: `TurboBoost::State::Manager.state_override_block`
56
- server_state_hash = {}
57
-
58
- # State the client expects... related to optimistic UI updates
59
- # i.e. Changes made on the client before making this request
60
- header_state_hash = {}
61
-
62
- # Apply server state overrides (i.e. state stored in databases like Redis, Postgres, etc...)
63
- if TurboBoost::Commands.config.apply_server_state_overrides
64
- begin
65
- state_override_block = self.class.state_override_block(controller)
66
- if state_override_block
67
- server_state_hash = controller.instance_eval(&state_override_block).with_indifferent_access
68
- server_state_hash.each { |key, val| self[key] = val }
69
- end
70
- rescue => error
71
- Rails.logger.error "Failed to apply `state_override_block` configured in #{controller.class.name} to TurboBoost::State! #{error.message}"
72
- end
73
- end
74
-
75
- # Apply client state overrides (i.e. optimistic state)
76
- # NOTE: Client state HTTP headers are only sent if/when state has changed on the client (only the changes are sent).
77
- # This prevents race conditions (state mismatch) caused when frame and XHR requests emit immediately
78
- # before the <meta id="turbo-boost"> has been updated with the latest state from the server.
79
- if TurboBoost::Commands.config.apply_client_state_overrides
80
- begin
81
- header_state_hash = TurboBoost::State.deserialize_base64(header).with_indifferent_access
82
- header_state_hash.each { |key, val| self[key] = val }
83
- rescue => error
84
- Rails.logger.error "Failed to apply client state from HTTP headers to TurboBoost::State! #{error.message}"
85
- end
86
- end
87
-
88
- @cookie_data = cookie_state_hash
89
- @header_data = header_state_hash
90
- @server_data = server_state_hash
91
- rescue => error
92
- Rails.logger.error "Failed to construct TurboBoost::State! #{error.message}"
93
- ensure
94
- @state ||= TurboBoost::State.new
95
- end
96
-
97
- delegate :cache_key, to: :state
98
-
99
- # Same implementation as ActionController::Base but with public visibility
100
- def cookies
101
- controller.request.cookie_jar
102
- end
103
-
104
- def [](*keys, default: nil)
105
- state.read(*keys, default: default)
106
- end
107
-
108
- def []=(*keys, value)
109
- state_will_change! if value != self[*keys]
110
- value.nil? ? state.delete(*keys) : state.write(*keys, value)
111
- end
112
-
113
- def provisional_state
114
- @provisional_state ||= TurboBoost::State::ProvisionalState.new(self)
115
- end
116
-
117
- alias_method :now, :provisional_state
118
-
119
- def clear
120
- provisional_state.clear
121
- state.clear
122
- end
123
-
124
- def payload
125
- provisional_state.clear
126
- state.shrink!
127
- state.payload
128
- end
129
-
130
- def ordinal_payload
131
- provisional_state.clear
132
- state.shrink!
133
- state.prune! max_bytesize: TurboBoost::Commands.config.max_cookie_size
134
- state.ordinal_payload
135
- end
136
-
137
- def write_cookie
138
- return unless changed? || cookie.blank?
139
- cookies.signed["turbo_boost.state"] = {value: ordinal_payload, path: "/", expires: 1.day.from_now}
140
- changes_applied
141
- rescue => error
142
- Rails.logger.error "Failed to write the TurboBoost::State cookie! #{error.message}"
143
- end
144
-
145
- private
146
-
147
- attr_reader :state
148
-
149
- def headers
150
- controller.request.headers.select { |(key, _)| key.match?(/TURBOBOOST_STATE/i) }.sort
151
- end
152
-
153
- # State that exists on the client.
154
- def header
155
- headers.map(&:last).join
156
- end
157
-
158
- # State that the server last rendered with.
159
- def cookie
160
- cookies.signed["turbo_boost.state"]
161
- end
162
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../state"
4
-
5
- class TurboBoost::State::ProvisionalState
6
- def initialize(state_manager)
7
- @state_manager = state_manager
8
- @keys = Set.new
9
- end
10
-
11
- attr_reader :keys
12
-
13
- def [](*keys, default: nil)
14
- state_manager[*keys, default: default]
15
- end
16
-
17
- def []=(*keys, value)
18
- key = TurboBoost::State.key_for(*keys)
19
- value.nil? ? self.keys.delete(key) : self.keys.add(key)
20
- state_manager[key] = value
21
- end
22
-
23
- def clear
24
- keys.each { |key| state_manager[key] = nil }
25
- keys.clear
26
- end
27
-
28
- private
29
-
30
- attr_reader :state_manager
31
- end
@@ -1,137 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "state/errors"
4
-
5
- class TurboBoost::State
6
- class << self
7
- def serialize_base64(data)
8
- Base64.urlsafe_encode64 data.to_json, padding: false
9
- end
10
-
11
- def deserialize_base64(string)
12
- return {} if string.blank?
13
- JSON.parse Base64.urlsafe_decode64(string)
14
- rescue => error
15
- raise TurboBoost::State::DeserializationError, "Unable to decode and parse Base64 string! \"#{string}\" #{error.message}"
16
- end
17
-
18
- def serialize(data)
19
- dump = Marshal.dump(data)
20
- deflated = Zlib::Deflate.deflate(dump, Zlib::BEST_COMPRESSION)
21
- Base64.urlsafe_encode64 deflated
22
- end
23
-
24
- def deserialize(string)
25
- return {} if string.blank?
26
- decoded = Base64.urlsafe_decode64(string)
27
- inflated = Zlib::Inflate.inflate(decoded)
28
- Marshal.load inflated
29
- rescue => error
30
- raise TurboBoost::State::DeserializationError, "Unable to decode, inflate, and load Base64 string! \"#{string}\" #{error.message}"
31
- end
32
-
33
- def key_for(*keys)
34
- keys.map { |key| key.try(:cache_key) || key.to_s }.join("/")
35
- end
36
- end
37
-
38
- def initialize(ordinal_payload = nil)
39
- @internal_data = {}.with_indifferent_access
40
- @internal_keys = []
41
-
42
- deserialize(ordinal_payload).each do |(key, value)|
43
- write key, value
44
- end
45
- end
46
-
47
- delegate :deserialize, :key_for, :serialize, :serialize_base64, to: "self.class"
48
- delegate :size, to: :internal_data
49
- delegate :include?, :has_key?, :key?, :member?, to: :internal_data
50
-
51
- def cache_key
52
- "turbo-boost/ui-state/#{Digest::SHA2.base64digest(payload)}"
53
- end
54
-
55
- def read(*keys, default: nil)
56
- value = internal_data[key_for(*keys)]
57
- value = write(*keys, default) if value.nil? && default
58
- value
59
- end
60
-
61
- def write(*keys, value)
62
- key = key_for(*keys)
63
- internal_keys.delete key if internal_keys.include?(key)
64
- internal_keys << key
65
- internal_data[key] = value
66
- value
67
- end
68
-
69
- def delete(*keys)
70
- key = key_for(*keys)
71
- internal_keys.delete key
72
- internal_data.delete key
73
- end
74
-
75
- def payload
76
- serialize_base64 internal_data
77
- end
78
-
79
- def ordinal_payload
80
- serialize internal_list
81
- end
82
-
83
- def clear
84
- internal_keys.clear
85
- internal_data.clear
86
- end
87
-
88
- def shrink!
89
- @internal_data = shrink(internal_data).with_indifferent_access
90
- @internal_keys = internal_keys & internal_data.keys
91
- end
92
-
93
- def prune!(max_bytesize: 2.kilobytes)
94
- return if internal_keys.blank?
95
- return if internal_data.blank?
96
-
97
- percentage = (max_bytesize > 0) ? ordinal_payload.bytesize / max_bytesize.to_f : 0
98
- while percentage > 1
99
- keys_to_keep = internal_keys.slice((internal_keys.length - (internal_keys.length / percentage).floor)..-1)
100
- keys_to_remove = internal_keys - keys_to_keep
101
- @internal_keys = keys_to_keep
102
- keys_to_remove.each { |key| internal_data.delete key }
103
- percentage = (max_bytesize > 0) ? ordinal_payload.bytesize / max_bytesize.to_f : 0
104
- end
105
- end
106
-
107
- # Returns a copy of the data as a Hash
108
- def to_h
109
- internal_data.deep_dup
110
- end
111
-
112
- private
113
-
114
- attr_reader :internal_keys
115
- attr_reader :internal_data
116
-
117
- def internal_list
118
- internal_keys.map { |key| [key, internal_data[key]] }
119
- end
120
-
121
- def shrink(obj)
122
- case obj
123
- when Array
124
- obj.each_with_object([]) do |value, memo|
125
- value = shrink(value)
126
- memo << value if value.present?
127
- end
128
- when Hash
129
- obj.each_with_object({}.with_indifferent_access) do |(key, value), memo|
130
- value = shrink(value)
131
- memo[key] = value if value.present?
132
- end
133
- else
134
- obj
135
- end
136
- end
137
- end