lib/io/event/timers.rb



# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2024-2025, by Samuel Williams.

require_relative "priority_heap"

class IO
	module Event
		# An efficient sorted set of timers.
		class Timers
			# A handle to a scheduled timer.
			class Handle
				# Initialize the handle with the given time and block.
				#
				# @parameter time [Float] The time at which the block should be called.
				# @parameter block [Proc] The block to call.
				def initialize(time, block)
					@time = time
					@block = block
				end
				
				# @attribute [Float] The time at which the block should be called.
				attr :time
				
				# @attribute [Proc | Nil] The block to call when the timer fires.
				attr :block
				
				# Compare the handle with another handle.
				#
				# @parameter other [Handle] The other handle to compare with.
				# @returns [Boolean] Whether the handle is less than the other handle.
				def < other
					@time < other.time
				end
				
				# Compare the handle with another handle.
				#
				# @parameter other [Handle] The other handle to compare with.
				# @returns [Boolean] Whether the handle is greater than the other handle.
				def > other
					@time > other.time
				end
				
				# Invoke the block.
				def call(...)
					@block.call(...)
				end
				
				# Cancel the timer.
				def cancel!
					@block = nil
				end
				
				# @returns [Boolean] Whether the timer has been cancelled.
				def cancelled?
					@block.nil?
				end
			end
			
			# Initialize the timers.
			def initialize
				@heap = PriorityHeap.new
				@scheduled = []
			end
			
			# @returns [Integer] The number of timers in the heap.
			def size
				flush!
				
				return @heap.size
			end
			
			# Schedule a block to be called at a specific time in the future.
			#
			# @parameter time [Float] The time at which the block should be called, relative to {#now}.
			# @parameter block [Proc] The block to call.
			def schedule(time, block)
				handle = Handle.new(time, block)
				
				@scheduled << handle
				
				return handle
			end
			
			# Schedule a block to be called after a specific time offset, relative to the current time as returned by {#now}.
			#
			# @parameter offset [#to_f] The time offset from the current time at which the block should be called.
			# @yields {|now| ...} When the timer fires.
			def after(offset, &block)
				schedule(self.now + offset.to_f, block)
			end
			
			# Compute the time interval until the next timer fires.
			#
			# @parameter now [Float] The current time.
			# @returns [Float | Nil] The time interval until the next timer fires, if any.
			def wait_interval(now = self.now)
				flush!
				
				while handle = @heap.peek
					if handle.cancelled?
						@heap.pop
					else
						return handle.time - now
					end
				end
			end
			
			# @returns [Float] The current time.
			def now
				::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
			end
			
			# Fire all timers that are ready to fire.
			#
			# @parameter now [Float] The current time.
			def fire(now = self.now)
				# Flush scheduled timers into the heap:
				flush!
				
				# Get the earliest timer:
				while handle = @heap.peek
					if handle.cancelled?
						@heap.pop
					elsif handle.time <= now
						# Remove the earliest timer from the heap:
						@heap.pop
						
						# Call the block:
						handle.call(now)
					else
						break
					end
				end
			end
			
			# Flush all scheduled timers into the heap.
			#
			# This is a small optimization which assumes that most timers (timeouts) will be cancelled.
			protected def flush!
				while handle = @scheduled.pop
					@heap.push(handle) unless handle.cancelled?
				end
			end
		end
	end
end