# frozen_string_literal: true require "concurrent/promise" require "dry/effects/provider" require "dry/effects/frame" module Dry module Effects module Providers class Defer < Provider[:defer] include ::Dry::Equalizer(:executor, inspect: false) option :executor, default: -> { :io } attr_reader :later_calls attr_reader :stack def defer(block, executor) stack = self.stack.dup at = Undefined.default(executor, self.executor) ::Concurrent::Promise.execute(executor: at) do Frame.spawn_fiber(stack, &block) end end def later(block, executor) if @later_calls.frozen? Instructions.Raise(Errors::EffectRejectedError.new(<<~MSG)) .later calls are not allowed, they would be processed by another stack. Add another defer handler to the current stack MSG else at = Undefined.default(executor, self.executor) stack = self.stack.dup @later_calls << ::Concurrent::Promise.new(executor: at) do Frame.spawn_fiber(stack, &block) end nil end end def wait(promises) if promises.is_a?(::Array) ::Concurrent::Promise.zip(*promises).value! else promises.value! end end # Yield the block with the handler installed # # @api private def call(options = {executor: Undefined}) unless Undefined.equal?(options[:executor]) @executor = options[:executor] end @stack = Frame.stack @later_calls = [] yield ensure later_calls.each(&:execute) end def dup if defined? @later_calls super.tap { |p| p.instance_variable_set(:@later_calls, EMPTY_ARRAY) } else super end end # @return [String] # @api public def represent info = [] info << executor.to_s if executor.is_a?(::Symbol) info << "call_later=#{later_calls.size}" if later_calls.any? if info.empty? "defer" else "defer[#{info.join(" ")}]" end end end end end end