turbo_boost-commands 0.1.3 → 0.2.0
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/app/assets/builds/@turbo-boost/commands.js +1 -1
- data/app/assets/builds/@turbo-boost/commands.js.map +4 -4
- data/app/assets/builds/@turbo-boost/commands.metafile.json +1 -1
- data/app/controllers/concerns/turbo_boost/commands/controller.rb +0 -6
- data/app/javascript/drivers/form.js +2 -2
- data/app/javascript/drivers/frame.js +2 -7
- data/app/javascript/drivers/index.js +1 -1
- data/app/javascript/drivers/method.js +29 -8
- data/app/javascript/drivers/window.js +2 -54
- data/app/javascript/headers.js +43 -0
- data/app/javascript/index.js +5 -9
- data/app/javascript/invoker.js +39 -0
- data/app/javascript/renderer.js +14 -12
- data/app/javascript/turbo.js +18 -27
- data/app/javascript/urls.js +8 -6
- data/app/javascript/version.js +1 -1
- data/lib/turbo_boost/commands/command.rb +6 -1
- data/lib/turbo_boost/commands/controller_pack.rb +6 -1
- data/lib/turbo_boost/commands/engine.rb +5 -3
- data/lib/turbo_boost/commands/middleware.rb +85 -0
- data/lib/turbo_boost/commands/runner.rb +83 -51
- data/lib/turbo_boost/commands/state.rb +11 -16
- data/lib/turbo_boost/commands/version.rb +1 -1
- metadata +8 -5
data/app/javascript/renderer.js
CHANGED
@@ -1,16 +1,18 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
const tail = '</html'
|
4
|
-
const headIndex = content.indexOf(head)
|
5
|
-
const tailIndex = content.lastIndexOf(tail)
|
6
|
-
if (headIndex >= 0 && tailIndex >= 0) {
|
7
|
-
const html = content.slice(content.indexOf('>', headIndex) + 1, tailIndex)
|
8
|
-
document.documentElement.innerHTML = html
|
9
|
-
}
|
1
|
+
const append = content => {
|
2
|
+
document.body.insertAdjacentHTML('beforeend', content)
|
10
3
|
}
|
11
4
|
|
12
|
-
|
13
|
-
|
5
|
+
// TODO: Revisit the "Replace" strategy after morph ships with Turbo 8
|
6
|
+
const replace = content => {
|
7
|
+
const parser = new DOMParser()
|
8
|
+
const doc = parser.parseFromString(content, 'text/html')
|
9
|
+
document.head.innerHTML = doc.head.innerHTML
|
10
|
+
document.body.innerHTML = doc.body.innerHTML
|
11
|
+
}
|
12
|
+
|
13
|
+
export const render = (strategy, content) => {
|
14
|
+
if (strategy.match(/^Append$/i)) return append(content)
|
15
|
+
if (strategy.match(/^Replace$/i)) return replace(content)
|
14
16
|
}
|
15
17
|
|
16
|
-
export default {
|
18
|
+
export default { render }
|
data/app/javascript/turbo.js
CHANGED
@@ -1,46 +1,37 @@
|
|
1
|
-
import
|
2
|
-
import renderer from './renderer'
|
3
|
-
import { dispatch } from './events'
|
1
|
+
import headers from './headers'
|
4
2
|
import lifecycle from './lifecycle'
|
3
|
+
import { dispatch } from './events'
|
4
|
+
import { render } from './renderer'
|
5
5
|
|
6
6
|
const frameSources = {}
|
7
7
|
|
8
|
-
// fires before making a turbo HTTP request
|
9
|
-
addEventListener('turbo:before-fetch-request', event => {
|
10
|
-
const frame = event.target.closest('turbo-frame')
|
11
|
-
const { fetchOptions } = event.detail
|
12
|
-
|
13
|
-
// command invoked and busy
|
14
|
-
if (self.TurboBoost?.Commands?.busy) {
|
15
|
-
let acceptHeaders = ['text/vnd.turbo-boost.html', fetchOptions.headers['Accept']]
|
16
|
-
acceptHeaders = acceptHeaders.filter(entry => entry && entry.trim().length > 0).join(', ')
|
17
|
-
fetchOptions.headers['Accept'] = acceptHeaders
|
18
|
-
}
|
19
|
-
})
|
20
|
-
|
21
8
|
// fires after receiving a turbo HTTP response
|
22
9
|
addEventListener('turbo:before-fetch-response', event => {
|
23
10
|
const frame = event.target.closest('turbo-frame')
|
11
|
+
if (frame?.id && frame?.src) frameSources[frame.id] = frame.src
|
12
|
+
|
24
13
|
const { fetchResponse: response } = event.detail
|
14
|
+
const header = response.header(headers.RESPONSE_HEADER)
|
25
15
|
|
26
|
-
if (
|
16
|
+
if (!header) return
|
27
17
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
}
|
18
|
+
// We'll take it from here Hotwire...
|
19
|
+
event.preventDefault()
|
20
|
+
const { statusCode } = response
|
21
|
+
const { strategy } = headers.tokenize(header)
|
33
22
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
}
|
23
|
+
// FAIL: Status outside the range of 200-399
|
24
|
+
if (statusCode < 200 || statusCode > 399) {
|
25
|
+
const error = `Server returned a ${status} status code! TurboBoost Commands require 2XX-3XX status codes.`
|
26
|
+
dispatch(lifecycle.events.clientError, document, { detail: { error, response } }, true)
|
38
27
|
}
|
28
|
+
|
29
|
+
response.responseHTML.then(content => render(strategy, content))
|
39
30
|
})
|
40
31
|
|
41
32
|
// fires when a frame element is navigated and finishes loading
|
42
33
|
addEventListener('turbo:frame-load', event => {
|
43
34
|
const frame = event.target.closest('turbo-frame')
|
44
|
-
frame.dataset.
|
35
|
+
frame.dataset.src = frameSources[frame.id] || frame.src || frame.dataset.src
|
45
36
|
delete frameSources[frame.id]
|
46
37
|
})
|
data/app/javascript/urls.js
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
-
|
1
|
+
const buildURL = path => {
|
2
2
|
const a = document.createElement('a')
|
3
|
-
a.href =
|
4
|
-
|
5
|
-
url.searchParams.set('tbc', JSON.stringify(payload))
|
6
|
-
return url
|
3
|
+
a.href = path
|
4
|
+
return new URL(a)
|
7
5
|
}
|
8
6
|
|
9
|
-
export default {
|
7
|
+
export default {
|
8
|
+
get commandInvocationURL() {
|
9
|
+
return buildURL('/turbo-boost-command-invocation')
|
10
|
+
}
|
11
|
+
}
|
data/app/javascript/version.js
CHANGED
@@ -1 +1 @@
|
|
1
|
-
export default '0.
|
1
|
+
export default '0.2.0'
|
@@ -90,7 +90,11 @@ class TurboBoost::Commands::Command
|
|
90
90
|
@turbo_streams = Set.new
|
91
91
|
end
|
92
92
|
|
93
|
-
# Abstract
|
93
|
+
# Abstract method to resolve state (default noop), override in subclassed commands
|
94
|
+
def resolve_state(client_state)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Abstract `perform` method, override in subclassed commands
|
94
98
|
def perform
|
95
99
|
raise NotImplementedError, "#{self.class.name} must implement the `perform` method!"
|
96
100
|
end
|
@@ -113,6 +117,7 @@ class TurboBoost::Commands::Command
|
|
113
117
|
end
|
114
118
|
|
115
119
|
# Same method signature as ActionView::Rendering#render (i.e. controller.view_context.render)
|
120
|
+
# Great for rendering partials with short-hand syntax sugar → `render "/path/to/partial"`
|
116
121
|
def render(options = {}, locals = {}, &block)
|
117
122
|
return controller.view_context.render(options, locals, &block) unless options.is_a?(Hash)
|
118
123
|
|
@@ -8,6 +8,7 @@ class TurboBoost::Commands::ControllerPack
|
|
8
8
|
attr_reader :runner, :command
|
9
9
|
|
10
10
|
delegate(
|
11
|
+
:command_state,
|
11
12
|
:command_aborted?,
|
12
13
|
:command_errored?,
|
13
14
|
:command_performed?,
|
@@ -15,7 +16,6 @@ class TurboBoost::Commands::ControllerPack
|
|
15
16
|
:command_requested?,
|
16
17
|
:command_succeeded?,
|
17
18
|
:controller,
|
18
|
-
:state,
|
19
19
|
to: :runner
|
20
20
|
)
|
21
21
|
|
@@ -23,4 +23,9 @@ class TurboBoost::Commands::ControllerPack
|
|
23
23
|
@runner = TurboBoost::Commands::Runner.new(controller)
|
24
24
|
@command = runner.command_instance
|
25
25
|
end
|
26
|
+
|
27
|
+
def state
|
28
|
+
ActiveSupport::Deprecation.warn "The `state` method has been deprecated. Please update to `command_state`."
|
29
|
+
command_state
|
30
|
+
end
|
26
31
|
end
|
@@ -7,6 +7,7 @@ require_relative "version"
|
|
7
7
|
require_relative "http_status_codes"
|
8
8
|
require_relative "errors"
|
9
9
|
require_relative "patches"
|
10
|
+
require_relative "middleware"
|
10
11
|
require_relative "command"
|
11
12
|
require_relative "controller_pack"
|
12
13
|
require_relative "../../../app/controllers/concerns/turbo_boost/commands/controller"
|
@@ -19,16 +20,17 @@ module TurboBoost::Commands
|
|
19
20
|
|
20
21
|
class Engine < ::Rails::Engine
|
21
22
|
config.turbo_boost_commands = ActiveSupport::OrderedOptions.new
|
22
|
-
config.turbo_boost_commands[:
|
23
|
+
config.turbo_boost_commands[:protect_from_forgery] = true
|
23
24
|
|
24
25
|
# must opt-in to state overrides
|
25
26
|
config.turbo_boost_commands[:apply_client_state_overrides] = false
|
26
27
|
config.turbo_boost_commands[:apply_server_state_overrides] = false
|
27
28
|
|
28
|
-
config.turbo_boost_commands
|
29
|
+
config.turbo_boost_commands[:precompile_assets] = true
|
29
30
|
|
30
|
-
initializer "turbo_boost_commands.configuration" do
|
31
|
+
initializer "turbo_boost_commands.configuration" do |app|
|
31
32
|
Mime::Type.register "text/vnd.turbo-boost.html", :turbo_boost
|
33
|
+
app.middleware.insert 0, TurboBoost::Commands::Middleware
|
32
34
|
|
33
35
|
ActiveSupport.on_load :action_controller_base do
|
34
36
|
# `self` is ActionController::Base
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class TurboBoost::Commands::Middleware
|
4
|
+
PATH = "/turbo-boost-command-invocation"
|
5
|
+
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
request = Rack::Request.new(env)
|
12
|
+
modify! request if modify?(request)
|
13
|
+
@app.call env
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
# Returns the MIME type for TurboBoost Command invocations.
|
19
|
+
def mime_type
|
20
|
+
Mime::Type.lookup_by_extension(:turbo_boost)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Indicates whether or not the request is a TurboBoost Command invocation that requires modifications
|
24
|
+
# before we hand things over to Rails.
|
25
|
+
#
|
26
|
+
# @param request [Rack::Request] the request to check
|
27
|
+
# @return [Boolean] true if the request is a TurboBoost Command invocation, false otherwise
|
28
|
+
def modify?(request)
|
29
|
+
return false unless request.post?
|
30
|
+
return false unless request.path.start_with?(PATH)
|
31
|
+
return false unless mime_type && request.env["HTTP_ACCEPT"]&.include?(mime_type)
|
32
|
+
true
|
33
|
+
rescue => error
|
34
|
+
puts "#{self.class.name} failed to determine if the request should be modified! #{error.message}"
|
35
|
+
false
|
36
|
+
end
|
37
|
+
|
38
|
+
# Modifies the given POST request so Rails sees it as GET.
|
39
|
+
#
|
40
|
+
# The posted JSON body content holds the TurboBoost Command meta data.
|
41
|
+
# The parsed JSON body is stored in the environment under the `turbo_boost_command` key.
|
42
|
+
#
|
43
|
+
# @example POST payload for: /turbo-boost-command-invocation
|
44
|
+
# {
|
45
|
+
# "id" => "turbo-command-f824ded1-a86e-4a36-9442-ea2165a64569", # unique command invocation id
|
46
|
+
# "name" => "IncrementCountCommand", # the command being invoked
|
47
|
+
# "elementId" => nil, # the triggering element's dom id
|
48
|
+
# "elementAttributes" => {"tag"=>"BUTTON", "checked"=>false, "disabled"=>false, "value"=>nil}, # the triggering element's attributes
|
49
|
+
# "startedAt" => 1708213193567, # the time the command was invoked
|
50
|
+
# "changedState" => {}, # the delta of optimistic state changes made on the client
|
51
|
+
# "clientState" => { # the state as it was on the client
|
52
|
+
# "command_token" => "IlU0dVVhNElFdkVCZVUi--a878d33d85ed9b9611c155ed1d7bb8785fb..."} # the command token used for forgery protection
|
53
|
+
# },
|
54
|
+
# "signedState" => "eyJfcmFpbHMiOnsiZGF0YSI6ImdpZDovL2R1VuaXZlcnNhbElEOjpFeH...", # the state as it was on the server at the time of the last command invocation
|
55
|
+
# "driver" => "frame", # the driver used to invoke the command
|
56
|
+
# "frameId" => "basic_command-turbo-frame", # the turbo-frame id (if applicable)
|
57
|
+
# "src" => "/basic_command.turbo_stream" # the URL to present to Rails (turbo-frame src, window location, etc.)
|
58
|
+
# }
|
59
|
+
#
|
60
|
+
# @param request [Rack::Request] the request to modify
|
61
|
+
def modify!(request)
|
62
|
+
params = JSON.parse(request.body.string)
|
63
|
+
uri = URI.parse(params["src"])
|
64
|
+
|
65
|
+
request.env.tap do |env|
|
66
|
+
# Store the command params in the environment
|
67
|
+
env["turbo_boost_command"] = params
|
68
|
+
|
69
|
+
# Change the method from POST to GET
|
70
|
+
env["REQUEST_METHOD"] = "GET"
|
71
|
+
|
72
|
+
# Update the URI, PATH_INFO, and QUERY_STRING
|
73
|
+
env["REQUEST_URI"] = uri.to_s if env.key?("REQUEST_URI")
|
74
|
+
env["PATH_INFO"] = uri.path
|
75
|
+
env["QUERY_STRING"] = uri.query.to_s
|
76
|
+
|
77
|
+
# Clear the body and related headers so the appears and behaves like a GET
|
78
|
+
env["rack.input"] = StringIO.new
|
79
|
+
env["CONTENT_LENGTH"] = "0"
|
80
|
+
env.delete("CONTENT_TYPE")
|
81
|
+
end
|
82
|
+
rescue => error
|
83
|
+
puts "#{self.class.name} failed to modify the request! #{error.message}"
|
84
|
+
end
|
85
|
+
end
|
@@ -4,6 +4,8 @@ require_relative "sanitizer"
|
|
4
4
|
require_relative "state"
|
5
5
|
|
6
6
|
class TurboBoost::Commands::Runner
|
7
|
+
RESPONSE_HEADER = "TurboBoost-Command"
|
8
|
+
|
7
9
|
SUPPORTED_MEDIA_TYPES = {
|
8
10
|
"text/html" => true,
|
9
11
|
"text/vnd.turbo-boost.html" => true,
|
@@ -16,8 +18,8 @@ class TurboBoost::Commands::Runner
|
|
16
18
|
@controller = controller
|
17
19
|
end
|
18
20
|
|
19
|
-
def
|
20
|
-
@
|
21
|
+
def command_state
|
22
|
+
@command_state ||= begin
|
21
23
|
sgid = command_params[:signed_state]
|
22
24
|
value = TurboBoost::Commands::State.from_sgid_param(sgid) if sgid
|
23
25
|
value || TurboBoost::Commands::State.new
|
@@ -25,7 +27,7 @@ class TurboBoost::Commands::Runner
|
|
25
27
|
end
|
26
28
|
|
27
29
|
def command_requested?
|
28
|
-
|
30
|
+
controller.request.env.key?("turbo_boost_command") || controller.params.key?("turbo_boost_command")
|
29
31
|
end
|
30
32
|
|
31
33
|
def command_valid?
|
@@ -38,25 +40,25 @@ class TurboBoost::Commands::Runner
|
|
38
40
|
end
|
39
41
|
|
40
42
|
# validate method
|
41
|
-
|
42
|
-
unless ancestors.any? { |a| a.public_instance_methods(false).any? command_method_name.to_sym }
|
43
|
+
unless command_instance.respond_to?(command_method_name)
|
43
44
|
raise TurboBoost::Commands::InvalidMethodError,
|
44
45
|
"`#{command_class_name}` does not define the public method `#{command_method_name}`!"
|
45
46
|
end
|
46
47
|
|
47
48
|
# validate csrf token
|
48
|
-
unless
|
49
|
+
unless valid_command_token?
|
49
50
|
raise TurboBoost::Commands::InvalidTokenError,
|
50
|
-
"Token mismatch! The token: #{
|
51
|
+
"Token mismatch! The token: #{client_command_token}` does not match the expected value of `#{server_command_token}`."
|
51
52
|
end
|
52
53
|
|
53
54
|
true
|
54
55
|
end
|
55
56
|
|
56
57
|
def command_params
|
57
|
-
return ActionController::Parameters.new
|
58
|
+
return ActionController::Parameters.new unless command_requested?
|
58
59
|
@command_params ||= begin
|
59
|
-
payload = parsed_command_params.
|
60
|
+
payload = parsed_command_params.transform_keys(&:underscore)
|
61
|
+
payload["element_attributes"]&.deep_transform_keys!(&:underscore)
|
60
62
|
ActionController::Parameters.new(payload).permit!
|
61
63
|
end
|
62
64
|
end
|
@@ -84,7 +86,7 @@ class TurboBoost::Commands::Runner
|
|
84
86
|
end
|
85
87
|
|
86
88
|
def command_instance
|
87
|
-
@command_instance ||= command_class&.new(controller,
|
89
|
+
@command_instance ||= command_class&.new(controller, command_state, command_params).tap do |instance|
|
88
90
|
instance&.add_observer self, :handle_command_event
|
89
91
|
end
|
90
92
|
end
|
@@ -109,6 +111,10 @@ class TurboBoost::Commands::Runner
|
|
109
111
|
!!command_instance&.succeeded?
|
110
112
|
end
|
111
113
|
|
114
|
+
def controller_action_allowed?
|
115
|
+
!controller_action_prevented?
|
116
|
+
end
|
117
|
+
|
112
118
|
def controller_action_prevented?
|
113
119
|
!!@controller_action_prevented
|
114
120
|
end
|
@@ -124,7 +130,8 @@ class TurboBoost::Commands::Runner
|
|
124
130
|
return if command_errored?
|
125
131
|
return if command_performing?
|
126
132
|
return if command_performed?
|
127
|
-
|
133
|
+
|
134
|
+
command_instance.resolve_state command_params[:changed_state]
|
128
135
|
command_instance.perform_with_callbacks command_method_name
|
129
136
|
end
|
130
137
|
|
@@ -137,17 +144,16 @@ class TurboBoost::Commands::Runner
|
|
137
144
|
render_response status: response_status
|
138
145
|
append_success_to_response
|
139
146
|
when TurboBoost::Commands::AbortError
|
140
|
-
render_response status: error.http_status_code,
|
147
|
+
render_response status: error.http_status_code, status_header: error.message
|
141
148
|
append_streams_to_response_body
|
142
149
|
when TurboBoost::Commands::PerformError
|
143
|
-
render_response status: error.http_status_code,
|
150
|
+
render_response status: error.http_status_code, status_header: error.message
|
144
151
|
append_error_to_response error
|
145
152
|
else
|
146
|
-
render_response status: :internal_server_error,
|
153
|
+
render_response status: :internal_server_error, status_header: error.message
|
147
154
|
append_error_to_response error
|
148
155
|
end
|
149
156
|
|
150
|
-
append_command_token_to_response_body
|
151
157
|
append_command_state_to_response_body
|
152
158
|
end
|
153
159
|
|
@@ -157,17 +163,16 @@ class TurboBoost::Commands::Runner
|
|
157
163
|
|
158
164
|
return if controller_action_prevented?
|
159
165
|
|
160
|
-
append_to_response_headers
|
161
|
-
append_command_token_to_response_body
|
162
166
|
append_command_state_to_response_body
|
167
|
+
append_to_response_headers if command_performed?
|
163
168
|
append_success_to_response if command_succeeded?
|
164
169
|
rescue => error
|
165
170
|
Rails.logger.error "TurboBoost::Commands::Runner failed to update the response! #{error.message}"
|
166
171
|
end
|
167
172
|
|
168
|
-
def render_response(html: "", status: nil,
|
169
|
-
controller.render html: html, layout: false, status: status || response_status
|
170
|
-
append_to_response_headers
|
173
|
+
def render_response(html: "", status: nil, status_header: nil)
|
174
|
+
controller.render html: html, layout: false, status: status || response_status # unless controller.performed?
|
175
|
+
append_to_response_headers status_header
|
171
176
|
end
|
172
177
|
|
173
178
|
def turbo_stream
|
@@ -175,9 +180,9 @@ class TurboBoost::Commands::Runner
|
|
175
180
|
end
|
176
181
|
|
177
182
|
def message_verifier
|
178
|
-
ActiveSupport::MessageVerifier.new Rails.application.secret_key_base, digest: "SHA256", url_safe: true
|
183
|
+
ActiveSupport::MessageVerifier.new "#{controller.request.session&.id}#{Rails.application.secret_key_base}", digest: "SHA256", url_safe: true
|
179
184
|
rescue
|
180
|
-
ActiveSupport::MessageVerifier.new Rails.application.secret_key_base, digest: "SHA256"
|
185
|
+
ActiveSupport::MessageVerifier.new "#{controller.request.session&.id}#{Rails.application.secret_key_base}", digest: "SHA256"
|
181
186
|
end
|
182
187
|
|
183
188
|
def handle_command_event(*args)
|
@@ -192,36 +197,34 @@ class TurboBoost::Commands::Runner
|
|
192
197
|
private
|
193
198
|
|
194
199
|
def parsed_command_params
|
195
|
-
@parsed_command_params ||=
|
200
|
+
@parsed_command_params ||= begin
|
201
|
+
params = controller.request.env["turbo_boost_command"]
|
202
|
+
params ||= JSON.parse(controller.params["turbo_boost_command"])
|
203
|
+
params || {}
|
204
|
+
end
|
196
205
|
end
|
197
206
|
|
198
207
|
def content_sanitizer
|
199
208
|
TurboBoost::Commands::Sanitizer.instance
|
200
209
|
end
|
201
210
|
|
202
|
-
|
203
|
-
|
204
|
-
@new_token ||= SecureRandom.alphanumeric(13)
|
211
|
+
def new_command_token
|
212
|
+
@new_command_token ||= SecureRandom.alphanumeric(13)
|
205
213
|
end
|
206
214
|
|
207
|
-
|
208
|
-
|
209
|
-
nil
|
215
|
+
def client_command_token
|
216
|
+
command_params.dig(:client_state, :command_token)
|
210
217
|
end
|
211
218
|
|
212
|
-
|
213
|
-
|
214
|
-
command_params[:token].to_s
|
219
|
+
def server_command_token
|
220
|
+
command_state[:command_token]
|
215
221
|
end
|
216
222
|
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
# unmasked_client_token = message_verifier.verify(client_token)
|
223
|
-
# unmasked_client_token == server_token
|
224
|
-
true
|
223
|
+
def valid_command_token?
|
224
|
+
return true unless Rails.configuration.turbo_boost_commands.protect_from_forgery
|
225
|
+
return false unless client_command_token.present?
|
226
|
+
return false unless server_command_token.present?
|
227
|
+
server_command_token == message_verifier.verify(client_command_token)
|
225
228
|
end
|
226
229
|
|
227
230
|
def should_redirect?
|
@@ -242,6 +245,28 @@ class TurboBoost::Commands::Runner
|
|
242
245
|
:unknown
|
243
246
|
end
|
244
247
|
|
248
|
+
# Indicates if a TurboStream template exists for the current action.
|
249
|
+
# Any template with the format of :turbo_boost or :turbo_stream format is considered a match.
|
250
|
+
# @return [Boolean] true if a TurboStream template exists, false otherwise
|
251
|
+
def turbo_stream_template_exists?
|
252
|
+
controller.lookup_context.exists? controller.action_name, controller.lookup_context.prefixes, formats: [:turbo_boost, :turbo_stream]
|
253
|
+
end
|
254
|
+
|
255
|
+
def rendering_strategy
|
256
|
+
# Use the replace strategy if the follow things are true:
|
257
|
+
#
|
258
|
+
# 1. The command was triggered by the WINDOW driver
|
259
|
+
# 2. After the command finishes, normal Rails mechanics resume (i.e. prevent_controller_action was not called)
|
260
|
+
# 3. There is NO TurboStream template for the current action (i.e. example.turbo_boost.erb, example.turbo_frame.erb)
|
261
|
+
#
|
262
|
+
# TODO: Revisit the "Replace" strategy after morph ships with Turbo 8
|
263
|
+
if command_params[:driver] == "window" && controller_action_allowed?
|
264
|
+
return "Replace" unless turbo_stream_template_exists?
|
265
|
+
end
|
266
|
+
|
267
|
+
"Append"
|
268
|
+
end
|
269
|
+
|
245
270
|
def append_success_to_response
|
246
271
|
append_success_event_to_response_body
|
247
272
|
append_streams_to_response_body
|
@@ -257,14 +282,16 @@ class TurboBoost::Commands::Runner
|
|
257
282
|
command_instance.turbo_streams.each { |stream| append_to_response_body stream }
|
258
283
|
end
|
259
284
|
|
260
|
-
def append_command_token_to_response_body
|
261
|
-
append_to_response_body turbo_stream.invoke("TurboBoost.Commands.token=", args: [new_token], camelize: false)
|
262
|
-
rescue => error
|
263
|
-
Rails.logger.error "TurboBoost::Commands::Runner failed to append the Command token to the response! #{error.message}"
|
264
|
-
end
|
265
|
-
|
266
285
|
def append_command_state_to_response_body
|
267
|
-
|
286
|
+
# use the masked token for the client state
|
287
|
+
command_state[:command_token] = message_verifier.generate(new_command_token)
|
288
|
+
client_state = command_state.to_json
|
289
|
+
|
290
|
+
# use the unmasked token for the signed (server) state
|
291
|
+
command_state[:command_token] = new_command_token
|
292
|
+
signed_state = command_state.to_sgid_param
|
293
|
+
|
294
|
+
append_to_response_body turbo_stream.invoke("TurboBoost.State.initialize", args: [client_state, signed_state], camelize: false)
|
268
295
|
rescue => error
|
269
296
|
Rails.logger.error "TurboBoost::Commands::Runner failed to append the Command state to the response! #{error.message}"
|
270
297
|
end
|
@@ -337,10 +364,15 @@ class TurboBoost::Commands::Runner
|
|
337
364
|
controller.response.set_header key.to_s, value.to_s
|
338
365
|
end
|
339
366
|
|
340
|
-
def append_to_response_headers(
|
367
|
+
def append_to_response_headers(status = nil)
|
341
368
|
return unless command_performed?
|
342
|
-
|
343
|
-
|
344
|
-
|
369
|
+
|
370
|
+
values = [
|
371
|
+
status || "#{controller.response.status} #{TurboBoost::Commands::HTTP_STATUS_CODES[controller.response.status]}".delete(","),
|
372
|
+
rendering_strategy,
|
373
|
+
command_name
|
374
|
+
]
|
375
|
+
|
376
|
+
append_response_header RESPONSE_HEADER, values.join(", ")
|
345
377
|
end
|
346
378
|
end
|
@@ -7,16 +7,10 @@ class TurboBoost::Commands::State
|
|
7
7
|
def from_sgid_param(sgid)
|
8
8
|
new URI::UID.from_sgid(sgid, for: name)&.decode
|
9
9
|
end
|
10
|
-
|
11
|
-
attr_reader :resolver
|
12
|
-
|
13
|
-
def assign_resolver(&block)
|
14
|
-
@resolver = block
|
15
|
-
end
|
16
10
|
end
|
17
11
|
|
18
12
|
def initialize(store = nil, provisional: false)
|
19
|
-
@store = store || ActiveSupport::Cache::MemoryStore.new(expires_in: 1.day, size:
|
13
|
+
@store = store || ActiveSupport::Cache::MemoryStore.new(expires_in: 1.day, size: 16.kilobytes)
|
20
14
|
@store.cleanup
|
21
15
|
@provisional = provisional
|
22
16
|
end
|
@@ -24,10 +18,17 @@ class TurboBoost::Commands::State
|
|
24
18
|
delegate :to_json, to: :to_h
|
25
19
|
delegate_missing_to :store
|
26
20
|
|
21
|
+
def dig(*keys)
|
22
|
+
to_h.with_indifferent_access.dig(*keys)
|
23
|
+
end
|
24
|
+
|
25
|
+
def merge!(hash = {})
|
26
|
+
hash.to_h.each { |key, val| self[key] = val }
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
27
30
|
def each
|
28
|
-
data.keys.each
|
29
|
-
yield key, self[key]
|
30
|
-
end
|
31
|
+
data.keys.each { |key| yield(key, self[key]) }
|
31
32
|
end
|
32
33
|
|
33
34
|
# Provisional state is for the current request/response and is exposed as `State#now`
|
@@ -36,12 +37,6 @@ class TurboBoost::Commands::State
|
|
36
37
|
!!@provisional
|
37
38
|
end
|
38
39
|
|
39
|
-
# TODO: implement state resolution
|
40
|
-
def resolve(client_state)
|
41
|
-
# return unless self.class.resolver
|
42
|
-
# self.class.resolver.call self, client_state
|
43
|
-
end
|
44
|
-
|
45
40
|
def now
|
46
41
|
return nil if provisional? # provisional state cannot hold child provisional state
|
47
42
|
@now ||= self.class.new(provisional: true)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: turbo_boost-commands
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nate Hopkins (hopsoft)
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-02-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -44,14 +44,14 @@ dependencies:
|
|
44
44
|
requirements:
|
45
45
|
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: 0.1.
|
47
|
+
version: 0.1.10
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: 0.1.
|
54
|
+
version: 0.1.10
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: universalid
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -394,7 +394,9 @@ files:
|
|
394
394
|
- app/javascript/drivers/window.js
|
395
395
|
- app/javascript/elements.js
|
396
396
|
- app/javascript/events.js
|
397
|
+
- app/javascript/headers.js
|
397
398
|
- app/javascript/index.js
|
399
|
+
- app/javascript/invoker.js
|
398
400
|
- app/javascript/lifecycle.js
|
399
401
|
- app/javascript/logger.js
|
400
402
|
- app/javascript/renderer.js
|
@@ -414,6 +416,7 @@ files:
|
|
414
416
|
- lib/turbo_boost/commands/engine.rb
|
415
417
|
- lib/turbo_boost/commands/errors.rb
|
416
418
|
- lib/turbo_boost/commands/http_status_codes.rb
|
419
|
+
- lib/turbo_boost/commands/middleware.rb
|
417
420
|
- lib/turbo_boost/commands/patches.rb
|
418
421
|
- lib/turbo_boost/commands/patches/action_view_helpers_tag_helper_tag_builder_patch.rb
|
419
422
|
- lib/turbo_boost/commands/runner.rb
|
@@ -442,7 +445,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
442
445
|
- !ruby/object:Gem::Version
|
443
446
|
version: '0'
|
444
447
|
requirements: []
|
445
|
-
rubygems_version: 3.5.
|
448
|
+
rubygems_version: 3.5.3
|
446
449
|
signing_key:
|
447
450
|
specification_version: 4
|
448
451
|
summary: Commands to help you build robust reactive applications with Rails & Hotwire.
|