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.
- checksums.yaml +4 -4
- data/README.md +21 -25
- data/app/assets/builds/@turbo-boost/commands.js +1 -1
- data/app/assets/builds/@turbo-boost/commands.js.map +4 -4
- data/app/controllers/concerns/turbo_boost/commands/controller.rb +1 -1
- data/app/javascript/drivers/form.js +0 -3
- data/app/javascript/drivers/window.js +0 -6
- data/app/javascript/index.js +27 -31
- data/app/javascript/state/index.js +17 -35
- data/app/javascript/state/observable.js +2 -3
- data/app/javascript/turbo.js +1 -8
- data/app/javascript/urls.js +1 -1
- data/app/javascript/version.js +1 -0
- data/lib/turbo_boost/commands/command.rb +3 -5
- data/lib/turbo_boost/commands/controller_pack.rb +5 -8
- data/lib/turbo_boost/commands/engine.rb +11 -2
- data/lib/turbo_boost/commands/errors.rb +2 -0
- data/lib/turbo_boost/commands/runner.rb +41 -38
- data/lib/turbo_boost/commands/state.rb +78 -0
- data/lib/turbo_boost/commands/version.rb +1 -1
- metadata +21 -12
- data/app/assets/builds/@turbo-boost/commands.metafile.json +0 -1
- data/app/javascript/meta.js +0 -19
- data/lib/turbo_boost/errors.rb +0 -6
- data/lib/turbo_boost/state/errors.rb +0 -6
- data/lib/turbo_boost/state/manager.rb +0 -162
- data/lib/turbo_boost/state/provisional_state.rb +0 -31
- data/lib/turbo_boost/state.rb +0 -137
@@ -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
|
data/lib/turbo_boost/state.rb
DELETED
@@ -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
|