turbo_boost-commands 0.0.17 → 0.1.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,29 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../state/manager"
4
3
  require_relative "runner"
5
4
 
6
5
  class TurboBoost::Commands::ControllerPack
7
6
  include TurboBoost::Commands::AttributeHydration
8
7
 
9
- attr_reader :controller, :state_manager, :runner, :command
10
- alias_method :state, :state_manager
8
+ attr_reader :runner, :command
11
9
 
12
10
  delegate(
13
11
  :command_aborted?,
14
12
  :command_errored?,
15
- :command_performing?,
16
13
  :command_performed?,
14
+ :command_performing?,
17
15
  :command_requested?,
18
16
  :command_succeeded?,
19
- :meta_tag,
17
+ :controller,
18
+ :state,
20
19
  to: :runner
21
20
  )
22
21
 
23
22
  def initialize(controller)
24
- @controller = controller
25
- @state_manager = TurboBoost::State::Manager.new(controller)
26
- @runner = TurboBoost::Commands::Runner.new(controller, state_manager)
23
+ @runner = TurboBoost::Commands::Runner.new(controller)
27
24
  @command = runner.command_instance
28
25
  end
29
26
  end
@@ -2,9 +2,9 @@
2
2
 
3
3
  require "turbo-rails"
4
4
  require "turbo_boost/streams"
5
+ require "universalid"
5
6
  require_relative "version"
6
7
  require_relative "http_status_codes"
7
- require_relative "../errors"
8
8
  require_relative "errors"
9
9
  require_relative "patches"
10
10
  require_relative "command"
@@ -19,13 +19,14 @@ module TurboBoost::Commands
19
19
 
20
20
  class Engine < ::Rails::Engine
21
21
  config.turbo_boost_commands = ActiveSupport::OrderedOptions.new
22
- config.turbo_boost_commands[:max_cookie_size] = ActionDispatch::Cookies::MAX_COOKIE_SIZE / 3
23
22
  config.turbo_boost_commands[:validate_client_token] = false
24
23
 
25
24
  # must opt-in to state overrides
26
25
  config.turbo_boost_commands[:apply_client_state_overrides] = false
27
26
  config.turbo_boost_commands[:apply_server_state_overrides] = false
28
27
 
28
+ config.turbo_boost_commands.precompile_assets = true
29
+
29
30
  initializer "turbo_boost_commands.configuration" do
30
31
  Mime::Type.register "text/vnd.turbo-boost.html", :turbo_boost
31
32
 
@@ -40,5 +41,13 @@ module TurboBoost::Commands
40
41
  ActionView::Helpers::TagHelper::TagBuilder.prepend TurboBoost::Commands::Patches::ActionViewHelpersTagHelperTagBuilderPatch
41
42
  end
42
43
  end
44
+
45
+ initializer "turbo_boost_commands.asset" do
46
+ config.after_initialize do |app|
47
+ if app.config.respond_to?(:assets) && app.config.turbo_boost_commands.precompile_assets
48
+ app.config.assets.precompile += %w[@turbo-boost/commands.js]
49
+ end
50
+ end
51
+ end
43
52
  end
44
53
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TurboBoost::Commands
4
+ class InvalidTokenError < StandardError; end
5
+
4
6
  class InvalidClassError < StandardError; end
5
7
 
6
8
  class InvalidMethodError < StandardError; end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "sanitizer"
4
+ require_relative "state"
4
5
 
5
6
  class TurboBoost::Commands::Runner
6
7
  SUPPORTED_MEDIA_TYPES = {
@@ -9,22 +10,18 @@ class TurboBoost::Commands::Runner
9
10
  "text/vnd.turbo-stream.html" => true
10
11
  }.freeze
11
12
 
12
- attr_reader :controller, :state_manager
13
+ attr_reader :controller
13
14
 
14
- def initialize(controller, state_manager)
15
+ def initialize(controller)
15
16
  @controller = controller
16
- @state_manager = state_manager
17
17
  end
18
18
 
19
- def meta_tag
20
- masked_token = message_verifier.generate(new_token)
21
- options = {
22
- id: "turbo-boost",
23
- name: "turbo-boost",
24
- content: masked_token,
25
- data: {busy: false, state: state_manager.payload}
26
- }
27
- controller.view_context.tag("meta", options).html_safe
19
+ def state
20
+ @state ||= begin
21
+ sgid = command_params[:signed_state]
22
+ value = TurboBoost::Commands::State.from_sgid_param(sgid) if sgid
23
+ value || TurboBoost::Commands::State.new
24
+ end
28
25
  end
29
26
 
30
27
  def command_requested?
@@ -48,15 +45,15 @@ class TurboBoost::Commands::Runner
48
45
 
49
46
  # validate csrf token
50
47
  unless valid_client_token?
51
- raise TurboBoost::InvalidTokenError,
52
- "CSRF token mismatch! The request header `TurboBoost-Token: #{client_token}` does not match the expected value of `#{server_token}`."
48
+ raise TurboBoost::Commands::InvalidTokenError,
49
+ "Token mismatch! The token: #{client_token}` does not match the expected value of `#{server_token}`."
53
50
  end
54
51
 
55
52
  true
56
53
  end
57
54
 
58
55
  def command_params
59
- return ActionController::Parameters.new if controller.params[:turbo_boost_command].nil?
56
+ return ActionController::Parameters.new if controller.params.keys.none?(/\A(tbc|turbo_boost_command)\z/o)
60
57
  @command_params ||= begin
61
58
  payload = parsed_command_params.deep_transform_keys(&:underscore)
62
59
  ActionController::Parameters.new(payload).permit!
@@ -86,7 +83,7 @@ class TurboBoost::Commands::Runner
86
83
  end
87
84
 
88
85
  def command_instance
89
- @command_instance ||= command_class&.new(controller, state_manager, command_params).tap do |instance|
86
+ @command_instance ||= command_class&.new(controller, state, command_params).tap do |instance|
90
87
  instance&.add_observer self, :handle_command_event
91
88
  end
92
89
  end
@@ -126,6 +123,7 @@ class TurboBoost::Commands::Runner
126
123
  return if command_errored?
127
124
  return if command_performing?
128
125
  return if command_performed?
126
+ state.resolve command_params[:client_state]
129
127
  command_instance.perform_with_callbacks command_method_name
130
128
  end
131
129
 
@@ -148,8 +146,8 @@ class TurboBoost::Commands::Runner
148
146
  append_error_to_response error
149
147
  end
150
148
 
151
- append_meta_tag_to_response_body # called before `write_cookie` so all state is emitted to the DOM
152
- state_manager.write_cookie # truncates state to stay within cookie size limits (4k)
149
+ append_command_token_to_response_body
150
+ append_command_state_to_response_body
153
151
  end
154
152
 
155
153
  def update_response
@@ -159,8 +157,8 @@ class TurboBoost::Commands::Runner
159
157
  return if controller_action_prevented?
160
158
 
161
159
  append_to_response_headers
162
- append_meta_tag_to_response_body # called before `write_cookie` so all state is emitted to the DOM
163
- state_manager.write_cookie # truncates state to stay within cookie size limits (4k)
160
+ append_command_token_to_response_body
161
+ append_command_state_to_response_body
164
162
  append_success_to_response if command_succeeded?
165
163
  rescue => error
166
164
  Rails.logger.error "TurboBoost::Commands::Runner failed to update the response! #{error.message}"
@@ -177,11 +175,8 @@ class TurboBoost::Commands::Runner
177
175
 
178
176
  def message_verifier
179
177
  ActiveSupport::MessageVerifier.new Rails.application.secret_key_base, digest: "SHA256", url_safe: true
180
- end
181
-
182
- # Same implementation as ActionController::Base but with public visibility
183
- def cookies
184
- controller.request.cookie_jar
178
+ rescue
179
+ ActiveSupport::MessageVerifier.new Rails.application.secret_key_base, digest: "SHA256"
185
180
  end
186
181
 
187
182
  def handle_command_event(*args)
@@ -196,31 +191,36 @@ class TurboBoost::Commands::Runner
196
191
  private
197
192
 
198
193
  def parsed_command_params
199
- @parsed_command_params ||= JSON.parse(controller.params[:turbo_boost_command])
194
+ @parsed_command_params ||= JSON.parse(controller.params[:tbc] || controller.params[:turbo_boost_command])
200
195
  end
201
196
 
202
197
  def content_sanitizer
203
198
  TurboBoost::Commands::Sanitizer.instance
204
199
  end
205
200
 
201
+ # TODO: revisit command token validation
206
202
  def new_token
207
- @new_token ||= SecureRandom.urlsafe_base64(12)
203
+ @new_token ||= SecureRandom.alphanumeric(13)
208
204
  end
209
205
 
206
+ # TODO: revisit command token validation
210
207
  def server_token
211
- cookies.encrypted["turbo_boost.token"]
208
+ nil
212
209
  end
213
210
 
211
+ # TODO: revisit command token validation
214
212
  def client_token
215
- (controller.request.headers["TurboBoost-Token"] || command_params[:token]).to_s
213
+ command_params[:token].to_s
216
214
  end
217
215
 
216
+ # TODO: revisit command token validation
218
217
  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
218
+ # return true unless Rails.configuration.turbo_boost_commands.validate_client_token
219
+ # return false unless client_token.present?
220
+ # return false unless message_verifier.valid_message?(client_token)
221
+ # unmasked_client_token = message_verifier.verify(client_token)
222
+ # unmasked_client_token == server_token
223
+ true
224
224
  end
225
225
 
226
226
  def should_redirect?
@@ -256,11 +256,16 @@ class TurboBoost::Commands::Runner
256
256
  command_instance.turbo_streams.each { |stream| append_to_response_body stream }
257
257
  end
258
258
 
259
- def append_meta_tag_to_response_body
260
- cookies.encrypted["turbo_boost.token"] = {value: new_token, path: "/"}
261
- append_to_response_body turbo_stream.invoke("morph", args: [meta_tag], selector: "#turbo-boost")
259
+ def append_command_token_to_response_body
260
+ append_to_response_body turbo_stream.invoke("TurboBoost.Commands.token=", args: [new_token], camelize: false)
261
+ rescue => error
262
+ Rails.logger.error "TurboBoost::Commands::Runner failed to append the Command token to the response! #{error.message}"
263
+ end
264
+
265
+ def append_command_state_to_response_body
266
+ append_to_response_body turbo_stream.invoke("TurboBoost.State.initialize", args: [state.to_json, state.to_sgid_param], camelize: false)
262
267
  rescue => error
263
- Rails.logger.error "TurboBoost::Commands::Runner failed to append the meta tag to the response! #{error.message}"
268
+ Rails.logger.error "TurboBoost::Commands::Runner failed to append the Command state to the response! #{error.message}"
264
269
  end
265
270
 
266
271
  def append_success_event_to_response_body
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TurboBoost::Commands::State
4
+ include Enumerable
5
+
6
+ class << self
7
+ def from_sgid_param(sgid)
8
+ new URI::UID.from_sgid(sgid, for: name)&.decode
9
+ end
10
+
11
+ attr_reader :resolver
12
+
13
+ def assign_resolver(&block)
14
+ @resolver = block
15
+ end
16
+ end
17
+
18
+ def initialize(store = nil, provisional: false)
19
+ @store = store || ActiveSupport::Cache::MemoryStore.new(expires_in: 1.day, size: 2.kilobytes)
20
+ @store.cleanup
21
+ @provisional = provisional
22
+ end
23
+
24
+ delegate :to_json, to: :to_h
25
+ delegate_missing_to :store
26
+
27
+ def each
28
+ data.keys.each do |key|
29
+ yield key, self[key]
30
+ end
31
+ end
32
+
33
+ # Provisional state is for the current request/response and is exposed as `State#now`
34
+ # Standard state is preserved across multiple requests
35
+ def provisional?
36
+ !!@provisional
37
+ end
38
+
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
+ def now
46
+ return nil if provisional? # provisional state cannot hold child provisional state
47
+ @now ||= self.class.new(provisional: true)
48
+ end
49
+
50
+ def cache_key
51
+ "TurboBoost::Commands::State/#{Digest::SHA2.base64digest(to_json)}"
52
+ end
53
+
54
+ def read(...)
55
+ now&.read(...) || store.read(...)
56
+ end
57
+
58
+ def [](...)
59
+ read(...)
60
+ end
61
+
62
+ def []=(...)
63
+ write(...)
64
+ end
65
+
66
+ def to_sgid_param
67
+ store.cleanup
68
+ URI::UID.build(store, include_blank: false).to_sgid_param for: self.class.name, expires_in: 1.week
69
+ end
70
+
71
+ private
72
+
73
+ attr_reader :store
74
+
75
+ def data
76
+ store.instance_variable_get(:@data) || {}
77
+ end
78
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module TurboBoost
4
4
  module Commands
5
- VERSION = "0.0.17"
5
+ VERSION = "0.1.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.0.17
4
+ version: 0.1.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-02-03 00:00:00.000000000 Z
11
+ date: 2024-02-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -44,14 +44,28 @@ dependencies:
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: 0.0.8
47
+ version: 0.1.7
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.0.8
54
+ version: 0.1.7
55
+ - !ruby/object:Gem::Dependency
56
+ name: universalid
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 0.1.7
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 0.1.7
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: capybara
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -344,7 +358,6 @@ files:
344
358
  - README.md
345
359
  - app/assets/builds/@turbo-boost/commands.js
346
360
  - app/assets/builds/@turbo-boost/commands.js.map
347
- - app/assets/builds/@turbo-boost/commands.metafile.json
348
361
  - app/assets/images/apple-touch-icon.png
349
362
  - app/assets/images/favicon-16x16.png
350
363
  - app/assets/images/favicon-32x32.png
@@ -369,7 +382,6 @@ files:
369
382
  - app/javascript/index.js
370
383
  - app/javascript/lifecycle.js
371
384
  - app/javascript/logger.js
372
- - app/javascript/meta.js
373
385
  - app/javascript/renderer.js
374
386
  - app/javascript/schema.js
375
387
  - app/javascript/state/index.js
@@ -390,12 +402,8 @@ files:
390
402
  - lib/turbo_boost/commands/patches/action_view_helpers_tag_helper_tag_builder_patch.rb
391
403
  - lib/turbo_boost/commands/runner.rb
392
404
  - lib/turbo_boost/commands/sanitizer.rb
405
+ - lib/turbo_boost/commands/state.rb
393
406
  - lib/turbo_boost/commands/version.rb
394
- - lib/turbo_boost/errors.rb
395
- - lib/turbo_boost/state.rb
396
- - lib/turbo_boost/state/errors.rb
397
- - lib/turbo_boost/state/manager.rb
398
- - lib/turbo_boost/state/provisional_state.rb
399
407
  homepage: https://github.com/hopsoft/turbo_boost-commands
400
408
  licenses:
401
409
  - MIT
@@ -411,7 +419,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
411
419
  requirements:
412
420
  - - ">="
413
421
  - !ruby/object:Gem::Version
414
- version: 2.7.5
422
+ version: '3.0'
415
423
  required_rubygems_version: !ruby/object:Gem::Requirement
416
424
  requirements:
417
425
  - - ">="
@@ -1 +0,0 @@
1
- {"inputs":{"app/javascript/meta.js":{"bytes":337,"imports":[],"format":"esm"},"app/javascript/events.js":{"bytes":783,"imports":[{"path":"<runtime>","kind":"import-statement","external":true}],"format":"esm"},"app/javascript/state/observable.js":{"bytes":920,"imports":[{"path":"app/javascript/meta.js","kind":"import-statement","original":"../meta"},{"path":"app/javascript/events.js","kind":"import-statement","original":"../events"}],"format":"esm"},"app/javascript/state/index.js":{"bytes":1835,"imports":[{"path":"app/javascript/meta.js","kind":"import-statement","original":"../meta"},{"path":"app/javascript/state/observable.js","kind":"import-statement","original":"./observable"},{"path":"app/javascript/events.js","kind":"import-statement","original":"../events"},{"path":"<runtime>","kind":"import-statement","external":true}],"format":"esm"},"app/javascript/renderer.js":{"bytes":475,"imports":[],"format":"esm"},"app/javascript/activity.js":{"bytes":279,"imports":[],"format":"esm"},"app/javascript/lifecycle.js":{"bytes":610,"imports":[{"path":"app/javascript/activity.js","kind":"import-statement","original":"./activity"},{"path":"app/javascript/events.js","kind":"import-statement","original":"./events"}],"format":"esm"},"app/javascript/turbo.js":{"bytes":1952,"imports":[{"path":"app/javascript/meta.js","kind":"import-statement","original":"./meta"},{"path":"app/javascript/state/index.js","kind":"import-statement","original":"./state"},{"path":"app/javascript/renderer.js","kind":"import-statement","original":"./renderer"},{"path":"app/javascript/events.js","kind":"import-statement","original":"./events"},{"path":"app/javascript/lifecycle.js","kind":"import-statement","original":"./lifecycle"},{"path":"<runtime>","kind":"import-statement","external":true}],"format":"esm"},"app/javascript/schema.js":{"bytes":226,"imports":[{"path":"<runtime>","kind":"import-statement","external":true}],"format":"esm"},"app/javascript/confirmation.js":{"bytes":947,"imports":[{"path":"app/javascript/events.js","kind":"import-statement","original":"./events"},{"path":"app/javascript/schema.js","kind":"import-statement","original":"./schema"}],"format":"esm"},"app/javascript/delegates.js":{"bytes":923,"imports":[{"path":"<runtime>","kind":"import-statement","external":true}],"format":"esm"},"app/javascript/elements.js":{"bytes":1451,"imports":[{"path":"app/javascript/schema.js","kind":"import-statement","original":"./schema"},{"path":"app/javascript/lifecycle.js","kind":"import-statement","original":"./lifecycle"}],"format":"esm"},"app/javascript/drivers/form.js":{"bytes":384,"imports":[{"path":"app/javascript/meta.js","kind":"import-statement","original":"../meta"}],"format":"esm"},"app/javascript/urls.js":{"bytes":240,"imports":[],"format":"esm"},"app/javascript/drivers/frame.js":{"bytes":218,"imports":[{"path":"app/javascript/urls.js","kind":"import-statement","original":"../urls"},{"path":"<runtime>","kind":"import-statement","external":true}],"format":"esm"},"app/javascript/drivers/method.js":{"bytes":265,"imports":[{"path":"app/javascript/urls.js","kind":"import-statement","original":"../urls"},{"path":"<runtime>","kind":"import-statement","external":true}],"format":"esm"},"app/javascript/drivers/window.js":{"bytes":2033,"imports":[{"path":"app/javascript/meta.js","kind":"import-statement","original":"../meta"},{"path":"app/javascript/state/index.js","kind":"import-statement","original":"../state"},{"path":"app/javascript/events.js","kind":"import-statement","original":"../events"},{"path":"app/javascript/lifecycle.js","kind":"import-statement","original":"../lifecycle"},{"path":"app/javascript/urls.js","kind":"import-statement","original":"../urls"},{"path":"app/javascript/renderer.js","kind":"import-statement","original":"../renderer"},{"path":"<runtime>","kind":"import-statement","external":true}],"format":"esm"},"app/javascript/drivers/index.js":{"bytes":2044,"imports":[{"path":"app/javascript/elements.js","kind":"import-statement","original":"../elements"},{"path":"app/javascript/drivers/form.js","kind":"import-statement","original":"./form"},{"path":"app/javascript/drivers/frame.js","kind":"import-statement","original":"./frame"},{"path":"app/javascript/drivers/method.js","kind":"import-statement","original":"./method"},{"path":"app/javascript/drivers/window.js","kind":"import-statement","original":"./window"}],"format":"esm"},"app/javascript/logger.js":{"bytes":729,"imports":[{"path":"app/javascript/events.js","kind":"import-statement","original":"./events"}],"format":"esm"},"app/javascript/uuids.js":{"bytes":202,"imports":[],"format":"esm"},"app/javascript/index.js":{"bytes":3558,"imports":[{"path":"app/javascript/turbo.js","kind":"import-statement","original":"./turbo"},{"path":"app/javascript/schema.js","kind":"import-statement","original":"./schema"},{"path":"app/javascript/events.js","kind":"import-statement","original":"./events"},{"path":"app/javascript/activity.js","kind":"import-statement","original":"./activity"},{"path":"app/javascript/confirmation.js","kind":"import-statement","original":"./confirmation"},{"path":"app/javascript/delegates.js","kind":"import-statement","original":"./delegates"},{"path":"app/javascript/drivers/index.js","kind":"import-statement","original":"./drivers"},{"path":"app/javascript/meta.js","kind":"import-statement","original":"./meta"},{"path":"app/javascript/elements.js","kind":"import-statement","original":"./elements"},{"path":"app/javascript/lifecycle.js","kind":"import-statement","original":"./lifecycle"},{"path":"app/javascript/logger.js","kind":"import-statement","original":"./logger"},{"path":"app/javascript/state/index.js","kind":"import-statement","original":"./state"},{"path":"app/javascript/urls.js","kind":"import-statement","original":"./urls"},{"path":"app/javascript/uuids.js","kind":"import-statement","original":"./uuids"},{"path":"<runtime>","kind":"import-statement","external":true}],"format":"esm"}},"outputs":{"app/assets/builds/@turbo-boost/commands.js.map":{"imports":[],"exports":[],"inputs":{},"bytes":37542},"app/assets/builds/@turbo-boost/commands.js":{"imports":[],"exports":["default"],"entryPoint":"app/javascript/index.js","inputs":{"app/javascript/meta.js":{"bytesInOutput":252},"app/javascript/events.js":{"bytesInOutput":500},"app/javascript/state/observable.js":{"bytesInOutput":405},"app/javascript/state/index.js":{"bytesInOutput":762},"app/javascript/renderer.js":{"bytesInOutput":263},"app/javascript/activity.js":{"bytesInOutput":173},"app/javascript/lifecycle.js":{"bytesInOutput":294},"app/javascript/turbo.js":{"bytesInOutput":1024},"app/javascript/schema.js":{"bytesInOutput":166},"app/javascript/confirmation.js":{"bytesInOutput":424},"app/javascript/delegates.js":{"bytesInOutput":473},"app/javascript/elements.js":{"bytesInOutput":782},"app/javascript/drivers/form.js":{"bytesInOutput":246},"app/javascript/urls.js":{"bytesInOutput":166},"app/javascript/drivers/frame.js":{"bytesInOutput":96},"app/javascript/drivers/method.js":{"bytesInOutput":130},"app/javascript/drivers/window.js":{"bytesInOutput":1298},"app/javascript/drivers/index.js":{"bytesInOutput":991},"app/javascript/logger.js":{"bytesInOutput":399},"app/javascript/uuids.js":{"bytesInOutput":159},"app/javascript/index.js":{"bytesInOutput":1686}},"bytes":11186}}}
@@ -1,19 +0,0 @@
1
- class Meta {
2
- get element() {
3
- return document.querySelector('meta[name="turbo-boost"]')
4
- }
5
-
6
- get token() {
7
- return this.element.getAttribute('content')
8
- }
9
-
10
- get busy() {
11
- return this.element.dataset.busy === 'true'
12
- }
13
-
14
- set busy(value) {
15
- return (this.element.dataset.busy = !!value)
16
- }
17
- }
18
-
19
- export default new Meta()
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TurboBoost
4
- class InvalidTokenError < StandardError
5
- end
6
- end
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class TurboBoost::State
4
- class DeserializationError < StandardError
5
- end
6
- end
@@ -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