Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Invoke commands via HTTP POST #127

Merged
merged 23 commits into from
Feb 21, 2024
Merged

Invoke commands via HTTP POST #127

merged 23 commits into from
Feb 21, 2024

Conversation

hopsoft
Copy link
Owner

@hopsoft hopsoft commented Feb 17, 2024

CleanShot 2024-02-21 at 09 08 45@2x

@hopsoft hopsoft marked this pull request as ready for review February 21, 2024 08:31
@@ -113,6 +117,7 @@ def dom_id_selector(...)
end

# Same method signature as ActionView::Rendering#render (i.e. controller.view_context.render)
# Great for rendering partials with short-hand syntax sugar → `render "/path/to/partial"`
def render(options = {}, locals = {}, &block)
return controller.view_context.render(options, locals, &block) unless options.is_a?(Hash)
Copy link
Sponsor Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

off topic, I might steal that for TBP

Mime::Type.register "text/vnd.turbo-boost.html", :turbo_boost
app.middleware.insert 0, TurboBoost::Commands::Middleware
Copy link
Sponsor Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I trust that you know that this is the right spot in the middleware chain to insert it 😅

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep we want it in as early as possible before rails or its middleware gets the request.

lib/turbo_boost/commands/runner.rb Outdated Show resolved Hide resolved
@@ -241,6 +245,28 @@ def response_type
:unknown
end

# Indicates if a TurboStream template exists for the current action.
# Any template with the format of :turbo_boost or :turbo_stream format is considered a match.
Copy link
Sponsor Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

turbo boost templates are new, right?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They've been supported for a while now given the Rails mechanics around mime types etc.

Copy link
Sponsor Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thats 🤯 . needs an example or README entry, I guess. I wasn't aware of it.

Comment on lines +286 to +292
# use the masked token for the client state
command_state[:command_token] = message_verifier.generate(new_command_token)
client_state = command_state.to_json

# use the unmasked token for the signed (server) state
command_state[:command_token] = new_command_token
signed_state = command_state.to_sgid_param
Copy link
Sponsor Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm, I think this would be cleaner if the State object just had two methods to render state

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will punt on this refactor for now.

@@ -14,4 +14,8 @@ class ApplicationCommand < TurboBoost::Commands::Command
rescue_from TurboBoost::Commands::PerformError do |error|
# do something...
end

def resolve_state(changed_state)
state.merge! changed_state
Copy link
Sponsor Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't that be command_state.merge? ?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's state when inside a command, and command_state when outside a command.

app/javascript/drivers/method.js Outdated Show resolved Hide resolved
app/javascript/drivers/method.js Outdated Show resolved Hide resolved

const invoke = (payload = {}) => {
try {
fetch(urls.commandInvocationURL.href, {
Copy link
Sponsor Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just wanted to mention the possibility to use https://github.com/rails/request.js here

app/javascript/invoker.js Outdated Show resolved Hide resolved
app/javascript/invoker.js Outdated Show resolved Hide resolved
Copy link
Contributor

@mhenrixon mhenrixon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apart from a potential spelling mistake, I see nothing but exciting possibilities and improvements with the code!

@@ -113,6 +117,7 @@ def dom_id_selector(...)
end

# Same method signature as ActionView::Rendering#render (i.e. controller.view_context.render)
# Great for rendering partials with short-hand syntax sugar → `render "/path/to/partial"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where I'd like to hook into TurboBoost and allow both a ViewComponent and a Phlex::View or Component to be rendered.

Thank you for the additional comment, now I know where to look.

# Any template with the format of :turbo_boost or :turbo_stream format is considered a match.
# @return [Boolean] true if a TurboStream template exists, false otherwise
def turbo_stream_template_exists?
controller.lookup_context.exists? controller.action_name, controller.lookup_context.prefixes, formats: [:turbo_boost, :turbo_stream]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might have to hook in here, too.

append_response_header "TurboBoost-Command-Status", "HTTP #{controller.response.status} #{TurboBoost::Commands::HTTP_STATUS_CODES[controller.response.status]}"

values = [
status || "#{controller.response.status} #{TurboBoost::Commands::HTTP_STATUS_CODES[controller.response.status]}".delete(","),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious question: why delete(",")? Looks a bit like a smell at first glance.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a safeguard as I use , as the value delimiter for multiple values stored under the single HTTP header now.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensures we can properly tokenize those values.

@store.cleanup
@provisional = provisional
end

delegate :to_json, to: :to_h
delegate_missing_to :store

def dig(*keys)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Phenomenal! Now that state is a little more complex this makes perfect sense.

append_error_to_response error
else
render_response status: :internal_server_error, headers: {"TurboBoost-Command-Status": error.message}
render_response status: :internal_server_error, status_header: error.message
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to complain about this, but then I saw the status_header usage underneath. It's a shame you sometimes need more context in the diff here.

app/javascript/drivers/method.js Outdated Show resolved Hide resolved
}

function append(content) {
document.body.insertAdjacentHTML('beforeend', content)
// TODO: Revisit the "Replace" strategy after morph ships with Turbo 8
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It already did ship ;)

fetchOptions.headers['Accept'] = acceptHeaders
}
})

// fires after receiving a turbo HTTP response
addEventListener('turbo:before-fetch-response', event => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is the meat of how we hook into turbo. I'm glad you asked me to have a look at the code; I am learning a lot

@hopsoft hopsoft merged commit 8a777dc into main Feb 21, 2024
6 checks passed
@hopsoft hopsoft deleted the hopsoft/post branch February 21, 2024 16:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants