turbo_boost-commands 0.1.3 → 0.2.0

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,16 +1,18 @@
1
- function replaceDocument(content) {
2
- const head = '<html'
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
- function append(content) {
13
- document.body.insertAdjacentHTML('beforeend', content)
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 { append, replaceDocument }
18
+ export default { render }
@@ -1,46 +1,37 @@
1
- import state from './state'
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 (frame) frameSources[frame.id] = frame.src
16
+ if (!header) return
27
17
 
28
- if (response.header('TurboBoost')) {
29
- if (response.statusCode < 200 || response.statusCode > 399) {
30
- const error = `Server returned a ${response.statusCode} status code! TurboBoost Commands require 2XX-3XX status codes.`
31
- dispatch(lifecycle.events.clientError, document, { detail: { ...event.detail, error } }, true)
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
- if (response.header('TurboBoost') === 'Append') {
35
- event.preventDefault()
36
- response.responseText.then(content => renderer.append(content))
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.turboBoostSrc = frameSources[frame.id] || frame.src || frame.dataset.turboBoostSrc
35
+ frame.dataset.src = frameSources[frame.id] || frame.src || frame.dataset.src
45
36
  delete frameSources[frame.id]
46
37
  })
@@ -1,9 +1,11 @@
1
- function build(urlString, payload = {}) {
1
+ const buildURL = path => {
2
2
  const a = document.createElement('a')
3
- a.href = urlString
4
- const url = new URL(a)
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 { build }
7
+ export default {
8
+ get commandInvocationURL() {
9
+ return buildURL('/turbo-boost-command-invocation')
10
+ }
11
+ }
@@ -1 +1 @@
1
- export default '0.1.3'
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 `perform` method, overridde in subclassed commands
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[:validate_client_token] = false
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.precompile_assets = true
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 state
20
- @state ||= begin
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
- command_params.present?
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
- ancestors = command_class.ancestors[0..command_class.ancestors.index(TurboBoost::Commands::Command) - 1]
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 valid_client_token?
49
+ unless valid_command_token?
49
50
  raise TurboBoost::Commands::InvalidTokenError,
50
- "Token mismatch! The token: #{client_token}` does not match the expected value of `#{server_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 if controller.params.keys.none?(/\A(tbc|turbo_boost_command)\z/o)
58
+ return ActionController::Parameters.new unless command_requested?
58
59
  @command_params ||= begin
59
- payload = parsed_command_params.deep_transform_keys(&:underscore)
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, state, command_params).tap do |instance|
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
- state.resolve command_params[:client_state]
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, headers: {"TurboBoost-Command-Status": error.message}
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, headers: {"TurboBoost-Command-Status": error.message}
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, headers: {"TurboBoost-Command-Status": error.message}
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, headers: {})
169
- controller.render html: html, layout: false, status: status || response_status
170
- append_to_response_headers headers.merge(TurboBoost: :Append)
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 ||= JSON.parse(controller.params[:tbc] || controller.params[:turbo_boost_command])
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
- # TODO: revisit command token validation
203
- def new_token
204
- @new_token ||= SecureRandom.alphanumeric(13)
211
+ def new_command_token
212
+ @new_command_token ||= SecureRandom.alphanumeric(13)
205
213
  end
206
214
 
207
- # TODO: revisit command token validation
208
- def server_token
209
- nil
215
+ def client_command_token
216
+ command_params.dig(:client_state, :command_token)
210
217
  end
211
218
 
212
- # TODO: revisit command token validation
213
- def client_token
214
- command_params[:token].to_s
219
+ def server_command_token
220
+ command_state[:command_token]
215
221
  end
216
222
 
217
- # TODO: revisit command token validation
218
- def valid_client_token?
219
- # return true unless Rails.configuration.turbo_boost_commands.validate_client_token
220
- # return false unless client_token.present?
221
- # return false unless message_verifier.valid_message?(client_token)
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
- append_to_response_body turbo_stream.invoke("TurboBoost.State.initialize", args: [state.to_json, state.to_sgid_param], camelize: false)
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(headers = {})
367
+ def append_to_response_headers(status = nil)
341
368
  return unless command_performed?
342
- headers.each { |key, val| append_response_header key, val }
343
- append_response_header "TurboBoost-Command", command_name
344
- append_response_header "TurboBoost-Command-Status", "HTTP #{controller.response.status} #{TurboBoost::Commands::HTTP_STATUS_CODES[controller.response.status]}"
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: 2.kilobytes)
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 do |key|
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)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module TurboBoost
4
4
  module Commands
5
- VERSION = "0.1.3"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
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.1.3
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-03-14 00:00:00.000000000 Z
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.8
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.8
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.6
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.