turbo_boost-commands 0.0.18 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

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