Skip to content

Commit

Permalink
Add current and all states
Browse files Browse the repository at this point in the history
  • Loading branch information
hopsoft committed May 20, 2024
1 parent 3109cc5 commit de66581
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 30 deletions.
2 changes: 1 addition & 1 deletion .standard.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ruby_version: 2.7
ruby_version: 3.0
format: progress
parallel: true

Expand Down
4 changes: 2 additions & 2 deletions lib/turbo_boost/commands/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,11 @@ def initialize(controller, state, params = {})
@state = state
@params = params
@turbo_streams = Set.new
resove_state if TurboBoost::Commands.config.resolve_state
resolve_state if TurboBoost::Commands.config.resolve_state
end

# Abstract method to resolve state (default: noop)
# Override in subclassed commands
# Override in subclassed commands to resolve unsigned/optimistic client state with signed/server state
def resolve_state
end

Expand Down
8 changes: 4 additions & 4 deletions lib/turbo_boost/commands/middlewares/exit_middleware.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# frozen_string_literal: true

class TurboBoost::Commands::ExitMiddleware
BODY_PATTERN = /<\/\s*body/io.freeze
TURBO_FRAME_PATTERN = /<\/\s*turbo-frame/io.freeze
TURBO_STREAM_PATTERN = /<\/\s*turbo-stream/io.freeze
TAIL_PATTERN = /\z/io.freeze
BODY_PATTERN = /<\/\s*body/io
TURBO_FRAME_PATTERN = /<\/\s*turbo-frame/io
TURBO_STREAM_PATTERN = /<\/\s*turbo-stream/io
TAIL_PATTERN = /\z/io

def initialize(app)
@app = app
Expand Down
65 changes: 50 additions & 15 deletions lib/turbo_boost/commands/state.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,78 @@
require "digest/md5"
require_relative "state_store"

# Class that encapsulates all the various forms of state.
#
# 1. `page` - Client-side transient page state used for rendering remembered element attributes
# 2. `now` - Server-side state for the current render only (discarded after rendering)
# 3. `signed` - Server-side state that persists across renders (state that was used for the last server-side render)
# 4. `unsigned` - Client-side state (optimistic client-side changes)
# 5. `current` - Combined server-side state (signed + now)
# 6. `all` - All state except unsigned (signed + now + page)
#
class TurboBoost::Commands::State
include Enumerable

def initialize(payload = {})
payload = payload.respond_to?(:to_unsafe_h) ? payload.to_unsafe_h : payload.to_h
payload = payload.with_indifferent_access

@store = HashWithIndifferentAccess.new
@store[:_now] = {}.with_indifferent_access
@store[:_page] = payload.fetch(:page, {}).with_indifferent_access
@store[:_unsigned] = payload.fetch(:unsigned, {}).with_indifferent_access
@store[:_signed] = TurboBoost::Commands::StateStore.new(payload.fetch(:signed, {}))
@now = {}.with_indifferent_access
@page = payload.fetch(:page, {}).with_indifferent_access
@signed = TurboBoost::Commands::StateStore.new(payload.fetch(:signed, {}))
@unsigned = payload.fetch(:unsigned, {}).with_indifferent_access
end

# Client-side transient page state used for rendering remembered element attributes
# @return [HashWithIndifferentAccess]
attr_reader :page

# Server-side state for the current render only (similar to flash.now)
# @note Discarded after rendering
# @return [HashWithIndifferentAccess]
attr_reader :now

# Server-side state that persists across renders
# This is the state that was used for the last server-side render (untampered by the client)
# @return [TurboBoost::Commands::StateStore]
attr_reader :signed

# @note Most state will interactions work with the signed state, so we delegate missing methods to it.
delegate_missing_to :signed
delegate :each, to: :all

def page
@store[:_page]
end
# Client-side state (optimistic client-side changes)
# @note There is a hook on Command instances to resolve state `Command#resolve_state`,
# where Command authors can determine how to properly handle optimistic client-side state.
# @return [HashWithIndifferentAccess]
attr_reader :unsigned
alias_method :optimistic, :unsigned

def now
@store[:_now]
# Combined server-side state (signed + now)
# @return [HashWithIndifferentAccess]
def current
signed.to_h.merge now
end

def signed
@store[:_signed]
end
delegate :each, to: :current

# All state except unsigned (page + current).
# @return [HashWithIndifferentAccess]
def all
signed.to_h.merge now
page.merge current
end

# Returns a cache key representing "all" state
def cache_key
"TurboBoost::Commands::State/#{Digest::MD5.base64digest(all.to_s)}"
end

# A JSON representation of state that can be sent to the client
#
# Includes the following keys:
# * `signed` - The signed state (String)
# * `unsigned` - The unsigned state (Hash)
#
# @return [String]
def to_json
{signed: signed.to_sgid_param, unsigned: signed.to_h}.to_json(camelize: false)
end
Expand Down
4 changes: 0 additions & 4 deletions test/dummy/app/commands/application_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,4 @@ class ApplicationCommand < TurboBoost::Commands::Command
rescue_from TurboBoost::Commands::PerformError do |error|
# do something...
end

def resolve_state(changed_state)
state.merge! changed_state
end
end
17 changes: 13 additions & 4 deletions test/turbo_boost/commands/state_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ class TurboBoost::Commands::StateTest < ActiveSupport::TestCase
assert_equal 2, state.signed[:b]
end

test "all with values" do
test "current with values" do
state = TurboBoost::Commands::State.new
state[:a] = 1
state.now[:b] = 2
assert_equal 1, state.all[:a]
assert_equal 2, state.all[:b]
assert_equal 1, state.current[:a]
assert_equal 2, state.current[:b]
end

test "cache_key" do
Expand All @@ -67,14 +67,23 @@ class TurboBoost::Commands::StateTest < ActiveSupport::TestCase
assert_equal "TurboBoost::Commands::State/8tCoYhaQyQn3uuW/t6+R1g==", state.cache_key
end

test "cache_key with values and now values" do
test "cache_key with now values" do
state = TurboBoost::Commands::State.new
state[:a] = "foo"
state[:b] = "bar"
state.now[:c] = "baz"
assert_equal "TurboBoost::Commands::State/MQtKVwc4zxFndIIVpzDcsQ==", state.cache_key
end

test "cache_key with page values" do
state = TurboBoost::Commands::State.new
state[:a] = "foo"
state[:b] = "bar"
state.now[:c] = "baz"
state.page[:dom_id] = {a: 1, b: 2, "aria-test": "foo", "data-test": "bar"}
assert_equal "TurboBoost::Commands::State/MOSYI9EFo6uN5FHq0+kU5w==", state.cache_key
end

test "to_json" do
state = TurboBoost::Commands::State.new
actual = JSON.parse(state.to_json)
Expand Down

0 comments on commit de66581

Please sign in to comment.