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.

@@ -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.