src/derails/passive_resistance/isolated_execution_state.rb
# frozen_string_literal: true

module PassiveResistance
  module IsolatedExecutionState # :nodoc:
    @isolation_level = nil

    Thread.attr_accessor :passive_resistance_execution_state
    Fiber.attr_accessor :passive_resistance_execution_state

    class << self
      attr_reader :isolation_level, :scope

      def isolation_level=(level)
        return if level == @isolation_level

        unless %i(thread fiber).include?(level)
          raise ArgumentError, "isolation_level must be `:thread` or `:fiber`, got: `#{level.inspect}`"
        end

        clear if @isolation_level

        @scope =
          case level
          when :thread; Thread
          when :fiber; Fiber
          end

        @isolation_level = level
      end

      def [](key)
        if state = @scope.current.passive_resistance_execution_state
          state[key]
        end
      end

      def []=(key, value)
        state = (@scope.current.passive_resistance_execution_state ||= {})
        state[key] = value
      end

      def key?(key)
        @scope.current.passive_resistance_execution_state&.key?(key)
      end

      def delete(key)
        @scope.current.passive_resistance_execution_state&.delete(key)
      end

      def clear
        @scope.current.passive_resistance_execution_state&.clear
      end

      def context
        scope.current
      end

      def share_with(other, &block)
        # Action Controller streaming spawns a new thread and copy thread locals.
        # We do the same here for backward compatibility, but this is very much a hack
        # and streaming should be rethought.
        old_state, context.passive_resistance_execution_state = context.passive_resistance_execution_state, other.passive_resistance_execution_state.dup
        block.call
      ensure
        context.passive_resistance_execution_state = old_state
      end
    end

    self.isolation_level = :thread
  end
end
