From a2b7f4c3809a89cdf0886d8fc1e8b81ccd32c9b4 Mon Sep 17 00:00:00 2001 From: "Nate Hopkins (hopsoft)" Date: Sun, 19 May 2024 18:00:33 -0600 Subject: [PATCH] Introduce StateStore and bring back MemoryStore for state --- README.md | 2 +- .../commands/middlewares/entry_middleware.rb | 5 +- lib/turbo_boost/commands/state.rb | 32 +++++----- lib/turbo_boost/commands/state_store.rb | 58 +++++++++++++++++++ test/dummy/config/environments/test.rb | 2 +- test/dummy/db/schema.rb | 2 + 6 files changed, 78 insertions(+), 23 deletions(-) create mode 100644 lib/turbo_boost/commands/state_store.rb diff --git a/README.md b/README.md index 1d05d491..2ea2436f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@

- Lines of Code + Lines of Code diff --git a/lib/turbo_boost/commands/middlewares/entry_middleware.rb b/lib/turbo_boost/commands/middlewares/entry_middleware.rb index 039bca87..21cef496 100644 --- a/lib/turbo_boost/commands/middlewares/entry_middleware.rb +++ b/lib/turbo_boost/commands/middlewares/entry_middleware.rb @@ -49,8 +49,9 @@ def modify?(request) # "startedAt" => 1708213193567, # Time the command was invoked # "elementCache" => {...}, # Cache of ALL tracked element attributes (optimistic changes) # "state" => { # State ... TODO: HOPSOFT - # "signed" => "", # - ... - # "unsigned" => {...} # - ... + # "page" => {...}, # - transient page state (element attributes, etc.) + # "signed" => "", # - signed state used for the last server render (untampered) + # "unsigned" => {...} # - state with optimistic changes from the client # }, # "driver" => "frame", # Driver used to invoke the command # "frameId" => "...", # TurboFrame id (if applicable) diff --git a/lib/turbo_boost/commands/state.rb b/lib/turbo_boost/commands/state.rb index 49145555..b913b6d9 100644 --- a/lib/turbo_boost/commands/state.rb +++ b/lib/turbo_boost/commands/state.rb @@ -1,34 +1,34 @@ # frozen_string_literal: true +require_relative "state_store" + class TurboBoost::Commands::State def initialize(payload = {}) - payload = payload.to_h - @store ||= (payload[:unsigned] || {}).with_indifferent_access + payload = payload.to_h.with_indifferent_access - store[:_now] ||= {} - store[:_page] = payload[:page] || {} - store[:_signed] = if payload[:signed].present? - URI::UID.from_sgid(payload[:signed], for: self.class.name)&.decode - end - store[:_signed] ||= {} + @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, {})) end def current - signed.merge now + signed.to_h.merge now end delegate_missing_to :current def now - store[:_now] + @store[:_now] end def page - store[:_page] + @store[:_page] end def signed - store[:_signed] + @store[:_signed] end def cache_key @@ -36,9 +36,7 @@ def cache_key end def to_json - uid = URI::UID.build(signed, include_blank: false) - sgid = uid.to_sgid_param(for: self.class.name, expires_in: 2.day) - {signed: sgid, unsigned: signed}.to_json(camelize: false) + {signed: signed.to_sgid_param, unsigned: signed.to_h}.to_json(camelize: false) end def tag_options(options) @@ -80,8 +78,4 @@ def tag_options(options) options end - - private - - attr_reader :store end diff --git a/lib/turbo_boost/commands/state_store.rb b/lib/turbo_boost/commands/state_store.rb new file mode 100644 index 00000000..736ce7b8 --- /dev/null +++ b/lib/turbo_boost/commands/state_store.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +class TurboBoost::Commands::StateStore < ActiveSupport::Cache::MemoryStore + include Enumerable + + SGID_PURPOSE = name.dup.freeze + + def initialize(payload = {}) + super(expires_in: 1.day, size: 16.kilobytes) + + begin + payload = URI::UID.from_sgid(payload, for: SGID_PURPOSE)&.decode if payload.is_a?(String) + payload ||= URI::UID.from_gid(payload, for: SGID_PURPOSE)&.decode if payload.is_a?(String) + rescue => error + Rails.logger.error "Failed to decode URI::UID when creating a TurboBoost::Commands::StateStore! #{error.message}" + payload = {} + end + + merge! payload + end + + alias_method :[], :read + alias_method :[]=, :write + + def to_h + (@data || {}).each_with_object({}) do |(key, entry), memo| + memo[key] = entry.value + end + end + + delegate :each, :dig, to: :to_h + + def merge!(other = {}) + other.to_h.each { |key, val| write key, val } + self + end + + def to_uid + cleanup + URI::UID.build to_h, include_blank: false + end + + def to_gid + to_uid.to_gid + end + + def to_gid_param + to_gid.to_param + end + + def to_sgid + to_uid.to_sgid for: SGID_PURPOSE, expires_in: 1.day + end + + def to_sgid_param + to_sgid.to_param + end +end diff --git a/test/dummy/config/environments/test.rb b/test/dummy/config/environments/test.rb index 006d26d5..5393f07e 100644 --- a/test/dummy/config/environments/test.rb +++ b/test/dummy/config/environments/test.rb @@ -30,7 +30,7 @@ config.cache_store = :null_store # Raise exceptions instead of rendering exception templates. - config.action_dispatch.show_exceptions = false + config.action_dispatch.show_exceptions = :none # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb index 56e7dfec..99ca2a59 100644 --- a/test/dummy/db/schema.rb +++ b/test/dummy/db/schema.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition.