Full Code of socketry/timers for AI

main 23bccc713a24 cached
35 files
51.4 KB
16.7k tokens
77 symbols
1 requests
Download .txt
Repository: socketry/timers
Branch: main
Commit: 23bccc713a24
Files: 35
Total size: 51.4 KB

Directory structure:
gitextract_wr79o4sy/

├── .editorconfig
├── .git-blame-ignore-revs
├── .github/
│   └── workflows/
│       ├── documentation-coverage.yaml
│       ├── documentation.yaml
│       ├── rubocop.yaml
│       ├── test-coverage.yaml
│       └── test.yaml
├── .gitignore
├── .mailmap
├── .rubocop.yml
├── config/
│   └── sus.rb
├── fixtures/
│   └── timer_quantum.rb
├── gems.rb
├── lib/
│   ├── timers/
│   │   ├── events.rb
│   │   ├── group.rb
│   │   ├── interval.rb
│   │   ├── priority_heap.rb
│   │   ├── timer.rb
│   │   ├── version.rb
│   │   └── wait.rb
│   └── timers.rb
├── license.md
├── readme.md
├── release.cert
├── test/
│   └── timers/
│       ├── events.rb
│       ├── group/
│       │   ├── cancel.rb
│       │   ├── every.rb
│       │   └── pause.rb
│       ├── group.rb
│       ├── performance.rb
│       ├── priority_heap.rb
│       ├── strict.rb
│       ├── timer.rb
│       └── wait.rb
└── timers.gemspec

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
indent_style = tab
indent_size = 2

[*.{yml,yaml}]
indent_style = space
indent_size = 2


================================================
FILE: .git-blame-ignore-revs
================================================
9180068e70c5b5c1fdb9a6c47f4d8f2553fc7104


================================================
FILE: .github/workflows/documentation-coverage.yaml
================================================
name: Documentation Coverage

on: [push, pull_request]

permissions:
  contents: read

env:
  CONSOLE_OUTPUT: XTerm
  COVERAGE: PartialSummary

jobs:
  validate:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v4
    - uses: ruby/setup-ruby@v1
      with:
        ruby-version: "3.4"
        bundler-cache: true
    
    - name: Validate coverage
      timeout-minutes: 5
      run: bundle exec bake decode:index:coverage lib


================================================
FILE: .github/workflows/documentation.yaml
================================================
name: Documentation

on:
  push:
    branches:
      - main

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages:
permissions:
  contents: read
  pages: write
  id-token: write

# Allow one concurrent deployment:
concurrency:
  group: "pages"
  cancel-in-progress: true

env:
  CONSOLE_OUTPUT: XTerm
  BUNDLE_WITH: maintenance

jobs:
  generate:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v4

    - uses: ruby/setup-ruby@v1
      with:
        ruby-version: "3.4"
        bundler-cache: true
    
    - name: Installing packages
      run: sudo apt-get install wget
    
    - name: Generate documentation
      timeout-minutes: 5
      run: bundle exec bake utopia:project:static --force no
    
    - name: Upload documentation artifact
      uses: actions/upload-pages-artifact@v3
      with:
        path: docs
  
  deploy:
    runs-on: ubuntu-latest
    
    environment:
      name: github-pages
      url: ${{steps.deployment.outputs.page_url}}
    
    needs: generate
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4


================================================
FILE: .github/workflows/rubocop.yaml
================================================
name: RuboCop

on: [push, pull_request]

permissions:
  contents: read

env:
  CONSOLE_OUTPUT: XTerm

jobs:
  check:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v4
    - uses: ruby/setup-ruby@v1
      with:
        ruby-version: ruby
        bundler-cache: true
    
    - name: Run RuboCop
      timeout-minutes: 10
      run: bundle exec rubocop


================================================
FILE: .github/workflows/test-coverage.yaml
================================================
name: Test Coverage

on: [push, pull_request]

permissions:
  contents: read

env:
  CONSOLE_OUTPUT: XTerm
  COVERAGE: PartialSummary

jobs:
  test:
    name: ${{matrix.ruby}} on ${{matrix.os}}
    runs-on: ${{matrix.os}}-latest
    
    strategy:
      matrix:
        os:
          - ubuntu
          - macos
        
        ruby:
          - "3.4"
    
    steps:
    - uses: actions/checkout@v4
    - uses: ruby/setup-ruby@v1
      with:
        ruby-version: ${{matrix.ruby}}
        bundler-cache: true
    
    - name: Run tests
      timeout-minutes: 5
      run: bundle exec bake test
    
    - uses: actions/upload-artifact@v4
      with:
        include-hidden-files: true
        if-no-files-found: error
        name: coverage-${{matrix.os}}-${{matrix.ruby}}
        path: .covered.db
  
  validate:
    needs: test
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v4
    - uses: ruby/setup-ruby@v1
      with:
        ruby-version: "3.4"
        bundler-cache: true
    
    - uses: actions/download-artifact@v4
    
    - name: Validate coverage
      timeout-minutes: 5
      run: bundle exec bake covered:validate --paths */.covered.db \;


================================================
FILE: .github/workflows/test.yaml
================================================
name: Test

on: [push, pull_request]

permissions:
  contents: read

env:
  CONSOLE_OUTPUT: XTerm

jobs:
  test:
    name: ${{matrix.ruby}} on ${{matrix.os}}
    runs-on: ${{matrix.os}}-latest
    continue-on-error: ${{matrix.experimental}}
    
    strategy:
      matrix:
        os:
          - ubuntu
          - macos
        
        ruby:
          - "3.1"
          - "3.2"
          - "3.3"
          - "3.4"
        
        experimental: [false]
        
        include:
          - os: ubuntu
            ruby: truffleruby
            experimental: true
          - os: ubuntu
            ruby: jruby
            experimental: true
          - os: ubuntu
            ruby: head
            experimental: true
    
    steps:
    - uses: actions/checkout@v4
    - uses: ruby/setup-ruby@v1
      with:
        ruby-version: ${{matrix.ruby}}
        bundler-cache: true
    
    - name: Run tests
      timeout-minutes: 10
      run: bundle exec bake test


================================================
FILE: .gitignore
================================================
/.bundle/
/pkg/
/gems.locked
/.covered.db
/external


================================================
FILE: .mailmap
================================================
Nicholas Evans <nevans@410labs.com>
Ron Evans <ron.evans@gmail.com>
Sean Gregory <skinnyjames@pigadmirersclub.net>
Utenmiki <utenmiki@gmail.com>
Donovan Keme <code@extremist.digital>
Donovan Keme <de@freed.network>
Donovan Keme <digitalextremist@users.noreply.github.com>
Utenmiki <takiy33@gmail.com>
Tommy Ong Gia Phu <tommyogp@gmail.com>
Ryunosuke Sato <tricknotes.rs@gmail.com>


================================================
FILE: .rubocop.yml
================================================
AllCops:
  DisabledByDefault: true

Layout/IndentationStyle:
  Enabled: true
  EnforcedStyle: tabs

Layout/InitialIndentation:
  Enabled: true

Layout/IndentationWidth:
  Enabled: true
  Width: 1

Layout/IndentationConsistency:
  Enabled: true
  EnforcedStyle: normal

Layout/BlockAlignment:
  Enabled: true

Layout/EndAlignment:
  Enabled: true
  EnforcedStyleAlignWith: start_of_line

Layout/BeginEndAlignment:
  Enabled: true
  EnforcedStyleAlignWith: start_of_line

Layout/ElseAlignment:
  Enabled: true

Layout/DefEndAlignment:
  Enabled: true

Layout/CaseIndentation:
  Enabled: true

Layout/CommentIndentation:
  Enabled: true

Layout/EmptyLinesAroundClassBody:
  Enabled: true

Layout/EmptyLinesAroundModuleBody:
  Enabled: true

Style/FrozenStringLiteralComment:
  Enabled: true

Style/StringLiterals:
  Enabled: true
  EnforcedStyle: double_quotes


================================================
FILE: config/sus.rb
================================================
# frozen_string_literal: true

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

require "covered/sus"
include Covered::Sus


================================================
FILE: fixtures/timer_quantum.rb
================================================
# frozen_string_literal: true

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

class TimerQuantum
	def self.resolve
		self.new.to_f
	end
	
	def to_f
		precision
	end
	
	private
	
	def precision
		@precision ||= self.measure_host_precision
	end
	
	def measure_host_precision(repeats: 100, duration: 0.01)
		# Measure the precision sleep using the monotonic clock:
		start_time = self.now
		repeats.times do
			sleep(duration)
		end
		end_time = self.now
		
		actual_duration = end_time - start_time
		expected_duration = repeats * duration
		
		if actual_duration < expected_duration
			warn "Invalid precision measurement: #{actual_duration} < #{expected_duration}"
			return 0.1
		end
		
		# This computes the overhead of sleep, called `repeats` times:
		return actual_duration - expected_duration
	end
	
	def now
		Process.clock_gettime(Process::CLOCK_MONOTONIC)
	end
end

TIMER_QUANTUM = TimerQuantum.resolve


================================================
FILE: gems.rb
================================================
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2012-2016, by Tony Arcieri.
# Copyright, 2014-2025, by Samuel Williams.
# Copyright, 2015, by Donovan Keme.

source "https://rubygems.org"

gemspec

group :maintenance, optional: true do
	gem "bake-modernize"
	gem "bake-gem"
end

group :test do
	gem "sus"
	gem "covered"
	gem "decode"
	gem "rubocop"
	
	gem "bake-test"
	gem "bake-test-external"
	
	gem "benchmark-ips"
	gem "ruby-prof", platform: :mri
end


================================================
FILE: lib/timers/events.rb
================================================
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2014-2022, by Samuel Williams.
# Copyright, 2014-2016, by Tony Arcieri.
# Copyright, 2014, by Lavir the Whiolet.
# Copyright, 2015, by Utenmiki.
# Copyright, 2015, by Donovan Keme.
# Copyright, 2021, by Wander Hillen.

require_relative "timer"
require_relative "priority_heap"

module Timers
	# Maintains a PriorityHeap of events ordered on time, which can be cancelled.
	class Events
		# Represents a cancellable handle for a specific timer event.
		class Handle
			include Comparable
			
			def initialize(time, callback)
				@time = time
				@callback = callback
			end
			
			# The absolute time that the handle should be fired at.
			attr_reader :time
			
			# Cancel this timer, O(1).
			def cancel!
				# The simplest way to keep track of cancelled status is to nullify the
				# callback. This should also be optimal for garbage collection.
				@callback = nil
			end
			
			# Has this timer been cancelled? Cancelled timer's don't fire.
			def cancelled?
				@callback.nil?
			end
			
			def <=> other
				@time <=> other.time
			end
			
			# Fire the callback if not cancelled with the given time parameter.
			def fire(time)
				@callback.call(time) if @callback
			end
		end
		
		def initialize
			# A sequence of handles, maintained in sorted order, future to present.
			# @sequence.last is the next event to be fired.
			@sequence = PriorityHeap.new
			@queue = []
		end
		
		# Add an event at the given time.
		def schedule(time, callback)
			flush!
			
			handle = Handle.new(time.to_f, callback)
			
			@queue << handle
			
			return handle
		end
		
		# Returns the first non-cancelled handle.
		def first
			merge!
			
			while (handle = @sequence.peek)
				return handle unless handle.cancelled?
				@sequence.pop
			end
		end
		
		# Returns the number of pending (possibly cancelled) events.
		def size
			@sequence.size + @queue.size
		end
		
		# Fire all handles for which Handle#time is less than the given time.
		def fire(time)
			merge!
			
			while handle = @sequence.peek and handle.time <= time
				@sequence.pop
				handle.fire(time)
			end
		end
		
		private
		
		# Move all non-cancelled timers from the pending queue to the priority heap
		def merge!
			while handle = @queue.pop
				next if handle.cancelled?
				
				@sequence.push(handle)
			end
		end
		
		def flush!
			while @queue.last&.cancelled?
				@queue.pop
			end
		end
	end
end


================================================
FILE: lib/timers/group.rb
================================================
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2014-2025, by Samuel Williams.
# Copyright, 2014-2016, by Tony Arcieri.
# Copyright, 2015, by Donovan Keme.
# Copyright, 2015, by Tommy Ong Gia Phu.

require "set"
require "forwardable"

require_relative "interval"
require_relative "timer"
require_relative "events"

module Timers
	# A collection of timers which may fire at different times
	class Group
		include Enumerable
		
		extend Forwardable
		def_delegators :@timers, :each, :empty?
		
		def initialize
			@events = Events.new
			
			@timers = Set.new
			@paused_timers = Set.new
			
			@interval = Interval.new
			@interval.start
		end
		
		# Scheduled events:
		attr_reader :events
		
		# Active timers:
		attr_reader :timers
		
		# Paused timers:
		attr_reader :paused_timers
		
		# Call the given block after the given interval. The first argument will be
		# the time at which the group was asked to fire timers for.
		def after(interval, &block)
			Timer.new(self, interval, false, &block)
		end
		
		# Call the given block immediately, and then after the given interval. The first
		# argument will be the time at which the group was asked to fire timers for.
		def now_and_after(interval, &block)
			yield
			after(interval, &block)
		end
		
		# Call the given block periodically at the given interval. The first
		# argument will be the time at which the group was asked to fire timers for.
		def every(interval, recur = true, &block)
			Timer.new(self, interval, recur, &block)
		end
		
		# Call the given block immediately, and then periodically at the given interval. The first
		# argument will be the time at which the group was asked to fire timers for.
		def now_and_every(interval, recur = true, &block)
			yield
			every(interval, recur, &block)
		end
		
		# Wait for the next timer and fire it. Can take a block, which should behave
		# like sleep(n), except that n may be nil (sleep forever) or a negative
		# number (fire immediately after return).
		def wait
			if block_given?
				yield wait_interval
				
				while (interval = wait_interval) && interval > 0
					yield interval
				end
			else
				while (interval = wait_interval) && interval > 0
					# We cannot assume that sleep will wait for the specified time, it might be +/- a bit.
					sleep interval
				end
			end
			
			fire
		end
		
		# Interval to wait until when the next timer will fire.
		# - nil: no timers
		# - -ve: timers expired already
		# -   0: timers ready to fire
		# - +ve: timers waiting to fire
		def wait_interval(offset = current_offset)
			if handle = @events.first
				handle.time - Float(offset)
			end
		end
		
		# Fire all timers that are ready.
		def fire(offset = current_offset)
			@events.fire(offset)
		end
		
		# Pause all timers.
		def pause
			@timers.dup.each(&:pause)
		end
		
		# Resume all timers.
		def resume
			@paused_timers.dup.each(&:resume)
		end
		
		alias continue resume
		
		# Delay all timers.
		def delay(seconds)
			@timers.each do |timer|
				timer.delay(seconds)
			end
		end
		
		# Cancel all timers.
		def cancel
			@timers.dup.each(&:cancel)
		end
		
		# The group's current time.
		def current_offset
			@interval.to_f
		end
	end
end


================================================
FILE: lib/timers/interval.rb
================================================
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2018-2022, by Samuel Williams.

module Timers
	# A collection of timers which may fire at different times
	class Interval
		# Get the current elapsed monotonic time.
		def initialize
			@total = 0.0
			@current = nil
		end
		
		def start
			return if @current
			
			@current = now
		end
		
		def stop
			return unless @current
			
			@total += duration
			
			@current = nil
		end
		
		def to_f
			@total + duration
		end
		
		protected def duration
			now - @current
		end
		
		protected def now
			::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
		end
	end
end


================================================
FILE: lib/timers/priority_heap.rb
================================================
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2021, by Wander Hillen.
# Copyright, 2021-2025, by Samuel Williams.

module Timers
	# A priority queue implementation using a standard binary minheap. It uses straight comparison
	# of its contents to determine priority. This works because a Handle from Timers::Events implements
	# the '<' operation by comparing the expiry time.
	# See <https://en.wikipedia.org/wiki/Binary_heap> for explanations of the main methods.
	class PriorityHeap
		def initialize
			# The heap is represented with an array containing a binary tree. See
			# https://en.wikipedia.org/wiki/Binary_heap#Heap_implementation for how this array
			# is built up.
			@contents = []
		end
		
		# Returns the earliest timer or nil if the heap is empty.
		def peek
			@contents[0]
		end
		
		# Returns the number of elements in the heap
		def size
			@contents.size
		end
		
		# Returns the earliest timer if the heap is non-empty and removes it from the heap.
		# Returns nil if the heap is empty. (and doesn't change the heap in that case)
		def pop
			# If the heap is empty:
			if @contents.empty?
				return nil
			end
			
			# If we have only one item, no swapping is required:
			if @contents.size == 1
				return @contents.pop
			end
			
			# Take the root of the tree:
			value = @contents[0]
			
			# Remove the last item in the tree:
			last = @contents.pop
			
			# Overwrite the root of the tree with the item:
			@contents[0] = last
			
			# Bubble it down into place:
			bubble_down(0)
			
			# validate!
			
			return value
		end
		
		# Inserts a new timer into the heap, then rearranges elements until the heap invariant is true again.
		def push(element)
			# Insert the item at the end of the heap:
			@contents.push(element)
			
			# Bubble it up into position:
			bubble_up(@contents.size - 1)
			
			# validate!
			
			return self
		end
		
		# Empties out the heap, discarding all elements
		def clear!
			@contents = []
		end

		# Validate the heap invariant. Every element except the root must not be smaller than
		# its parent element. Note that it MAY be equal.
		def valid?
			# notice we skip index 0 on purpose, because it has no parent
			(1..(@contents.size - 1)).all? { |e| @contents[e] >= @contents[(e - 1) / 2] }
		end

		private
		
		# Left here for reference, but unused.
		# def swap(i, j)
		# 	@contents[i], @contents[j] = @contents[j], @contents[i]
		# end
		
		def bubble_up(index)
			parent_index = (index - 1) / 2 # watch out, integer division!
			
			while index > 0 && @contents[index] < @contents[parent_index]
				# if the node has a smaller value than its parent, swap these nodes
				# to uphold the minheap invariant and update the index of the 'current'
				# node. If the node is already at index 0, we can also stop because that
				# is the root of the heap.
				# swap(index, parent_index)
				@contents[index], @contents[parent_index] = @contents[parent_index], @contents[index]
				
				index = parent_index
				parent_index = (index - 1) / 2 # watch out, integer division!
			end
		end
		
		def bubble_down(index)
			swap_value = 0
			swap_index = nil
			
			while true
				left_index = (2 * index) + 1
				left_value = @contents[left_index]
				
				if left_value.nil?
					# This node has no children so it can't bubble down any further.
					# We're done here!
					return
				end
				
				# Determine which of the child nodes has the smallest value:
				right_index = left_index + 1
				right_value = @contents[right_index]
				
				if right_value.nil? or right_value > left_value
					swap_value = left_value
					swap_index = left_index
				else
					swap_value = right_value
					swap_index = right_index
				end
				
				if @contents[index] < swap_value
					# No need to swap, the minheap invariant is already satisfied:
					return
				else
					# At least one of the child node has a smaller value than the current node, swap current node with that child and update current node for if it might need to bubble down even further:
					# swap(index, swap_index)
					@contents[index], @contents[swap_index] = @contents[swap_index], @contents[index]
					
					index = swap_index
				end
			end
		end
	end
end


================================================
FILE: lib/timers/timer.rb
================================================
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2014-2025, by Samuel Williams.
# Copyright, 2014-2017, by Tony Arcieri.
# Copyright, 2014, by Utenmiki.
# Copyright, 2014, by Lin Jen-Shin.
# Copyright, 2017, by Vít Ondruch.
# Copyright, 2025, by Patrik Wenger.

module Timers
	# An individual timer set to fire a given proc at a given time. A timer is
	# always connected to a Timer::Group but it would ONLY be in @group.timers
	# if it also has a @handle specified. Otherwise it is either PAUSED or has
	# been FIRED and is not recurring. You can manually enter this state by
	# calling #cancel and resume normal operation by calling #reset.
	class Timer
		include Comparable
		attr_reader :interval, :offset, :recurring
		
		def initialize(group, interval, recurring = false, offset = nil, &block)
			@group = group
			
			@interval = interval
			@recurring = recurring
			@block = block
			@offset = nil
			@handle = nil
			
			# If a start offset was supplied, use that, otherwise use the current timers offset.
			reset(offset || @group.current_offset)
		end
		
		def paused?
			@group.paused_timers.include? self
		end
		
		def pause
			return if paused?
			
			@group.timers.delete self
			@group.paused_timers.add self
			
			@handle.cancel! if @handle
			@handle = nil
		end
		
		def resume
			return unless paused?
			
			@group.paused_timers.delete self
			
			# This will add us back to the group:
			reset
		end
		
		alias continue resume
		
		# Extend this timer
		def delay(seconds)
			@handle.cancel! if @handle
			
			@offset += seconds
			
			@handle = @group.events.schedule(@offset, self)
		end
		
		# Cancel this timer. Do not call while paused.
		def cancel
			return unless @handle
			
			@handle.cancel! if @handle
			@handle = nil
			
			# This timer is no longer valid:
			@group.timers.delete(self) if @group
		end
		
		# Reset this timer. Do not call while paused.
		# @param offset [Numeric] the duration to add to the timer.
		def reset(offset = @group.current_offset)
			# This logic allows us to minimise the interaction with @group.timers.
			# A timer with a handle is always registered with the group.
			if @handle
				@handle.cancel!
			else
				@group.timers << self
			end
			
			@offset = Float(offset) + @interval
			
			@handle = @group.events.schedule(@offset, self)
		end
		
		# Fire the block.
		def fire(offset = @group.current_offset)
			if recurring == :strict
				# ... make the next interval strictly the last offset + the interval:
				reset(@offset)
			elsif recurring
				reset(offset)
			else
				@offset = offset
			end
			
			result = @block.call(offset, self)
			cancel unless recurring
			result
		end
		
		alias call fire
		
		# Number of seconds until next fire / since last fire
		def fires_in
			@offset - @group.current_offset if @offset
		end
		
		# Inspect a timer
		def inspect
			buffer = to_s[0..-2]
			
			if @offset
				delta_offset = @offset - @group.current_offset
				
				if delta_offset > 0
					buffer << " fires in #{delta_offset} seconds"
				else
					buffer << " fired #{delta_offset.abs} seconds ago"
				end
				
				buffer << ", recurs every #{interval}" if recurring
			end
			
			buffer << ">"
			
			return buffer
		end
	end
end


================================================
FILE: lib/timers/version.rb
================================================
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2012-2016, by Tony Arcieri.
# Copyright, 2014-2022, by Samuel Williams.
# Copyright, 2015, by Donovan Keme.

module Timers
	VERSION = "4.4.0"
end


================================================
FILE: lib/timers/wait.rb
================================================
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2014-2025, by Samuel Williams.
# Copyright, 2014-2016, by Tony Arcieri.
# Copyright, 2015, by Utenmiki.
# Copyright, 2015, by Donovan Keme.

require_relative "interval"

module Timers
	# An exclusive, monotonic timeout class.
	class Wait
		def self.for(duration, &block)
			if duration
				timeout = new(duration)
				
				timeout.while_time_remaining(&block)
			else
				# If there is no "duration" to wait for, we wait forever.
				loop do
					yield(nil)
				end
			end
		end
		
		def initialize(duration)
			@duration = duration
			@remaining = true
		end
		
		attr_reader :duration
		attr_reader :remaining
		
		# Yields while time remains for work to be done:
		def while_time_remaining
			@interval = Interval.new
			@interval.start
			
			yield @remaining while time_remaining?
		ensure
			@interval.stop
			@interval = nil
		end
		
		private
		
		def time_remaining?
			@remaining = (@duration - @interval.to_f)
			
			@remaining > 0
		end
	end
end


================================================
FILE: lib/timers.rb
================================================
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2012-2016, by Tony Arcieri.
# Copyright, 2012, by Ryan LeCompte.
# Copyright, 2012, by Nicholas Evans.
# Copyright, 2012, by Dimitrij Denissenko.
# Copyright, 2013, by Chuck Remes.
# Copyright, 2013, by Ron Evans.
# Copyright, 2013, by Sean Gregory.
# Copyright, 2013, by Utenmiki.
# Copyright, 2013, by Jeremy Hinegardner.
# Copyright, 2014, by Larry Lv.
# Copyright, 2014, by Bruno Enten.
# Copyright, 2014-2022, by Samuel Williams.
# Copyright, 2014, by Mike Bourgeous.

require_relative "timers/version"

require_relative "timers/group"
require_relative "timers/wait"


================================================
FILE: license.md
================================================
# MIT License

Copyright, 2012-2017, by Tony Arcieri.  
Copyright, 2012, by Ryan LeCompte.  
Copyright, 2012, by Jesse Cooke.  
Copyright, 2012, by Nicholas Evans.  
Copyright, 2012, by Dimitrij Denissenko.  
Copyright, 2013, by Chuck Remes.  
Copyright, 2013, by Ron Evans.  
Copyright, 2013, by Sean Gregory.  
Copyright, 2013-2015, by Utenmiki.  
Copyright, 2013, by Jeremy Hinegardner.  
Copyright, 2014, by Larry Lv.  
Copyright, 2014, by Bruno Enten.  
Copyright, 2014-2025, by Samuel Williams.  
Copyright, 2014, by Mike Bourgeous.  
Copyright, 2014, by Klaus Trainer.  
Copyright, 2014, by Lin Jen-Shin.  
Copyright, 2014, by Lavir the Whiolet.  
Copyright, 2015-2016, by Donovan Keme.  
Copyright, 2015, by Tommy Ong Gia Phu.  
Copyright, 2015, by Will Jessop.  
Copyright, 2016, by Ryunosuke Sato.  
Copyright, 2016, by Atul Bhosale.  
Copyright, 2017, by Vít Ondruch.  
Copyright, 2017-2020, by Olle Jonsson.  
Copyright, 2020, by Tim Smith.  
Copyright, 2021, by Wander Hillen.  
Copyright, 2022, by Yoshiki Takagi.  
Copyright, 2023, by Peter Goldstein.  
Copyright, 2025, by Patrik Wenger.  

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: readme.md
================================================
# Timers

Collections of one-shot and periodic timers, intended for use with event loops such as [async](https://github.com/socketry/async).

[![Development Status](https://github.com/socketry/timers/workflows/Test/badge.svg)](https://github.com/socketry/timers/actions?workflow=Test)

## Installation

Add this line to your application's Gemfile:

``` ruby
gem 'timers'
```

And then execute:

    $ bundle

Or install it yourself as:

    $ gem install timers

## Usage

Create a new timer group with `Timers::Group.new`:

``` ruby
require 'timers'

timers = Timers::Group.new
```

Schedule a proc to run after 5 seconds with `Timers::Group#after`:

``` ruby
five_second_timer = timers.after(5) { puts "Take five" }
```

The `five_second_timer` variable is now bound to a Timers::Timer object. To
cancel a timer, use `Timers::Timer#cancel`

Once you've scheduled a timer, you can wait until the next timer fires with `Timers::Group#wait`:

``` ruby
# Waits 5 seconds
timers.wait

# The script will now print "Take five"
```

You can schedule a block to run periodically with `Timers::Group#every`:

``` ruby
every_five_seconds = timers.every(5) { puts "Another 5 seconds" }

loop { timers.wait }
```

You can also schedule a block to run immediately and periodically with `Timers::Group#now_and_every`:

``` ruby
now_and_every_five_seconds = timers.now_and_every(5) { puts "Now and in another 5 seconds" }

loop { timers.wait }
```

If you'd like another method to do the waiting for you, e.g. `Kernel.select`,
you can use `Timers::Group#wait_interval` to obtain the amount of time to wait. When
a timeout is encountered, you can fire all pending timers with `Timers::Group#fire`:

``` ruby
loop do
  interval = timers.wait_interval
  ready_readers, ready_writers = select readers, writers, nil, interval

  if ready_readers || ready_writers
    # Handle IO
    ...
  else
    # Timeout!
    timers.fire
  end
end
```

You can also pause and continue individual timers, or all timers:

``` ruby
paused_timer = timers.every(5) { puts "I was paused" }

paused_timer.pause
10.times { timers.wait } # will not fire paused timer

paused_timer.resume
10.times { timers.wait } # will fire timer

timers.pause
10.times { timers.wait } # will not fire any timers

timers.resume
10.times { timers.wait } # will fire all timers
```

## Contributing

We welcome contributions to this project.

1.  Fork it.
2.  Create your feature branch (`git checkout -b my-new-feature`).
3.  Commit your changes (`git commit -am 'Add some feature'`).
4.  Push to the branch (`git push origin my-new-feature`).
5.  Create new Pull Request.

### Developer Certificate of Origin

In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.

### Community Guidelines

This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.


================================================
FILE: release.cert
================================================
-----BEGIN CERTIFICATE-----
MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11
ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK
CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz
MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd
MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj
bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2
9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW
sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE
e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN
XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss
RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn
tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM
zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW
xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O
BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs
aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs
aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE
cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl
xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/
c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp
8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws
JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP
eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
-----END CERTIFICATE-----


================================================
FILE: test/timers/events.rb
================================================
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2014-2025, by Samuel Williams.
# Copyright, 2014-2016, by Tony Arcieri.

require "timers/events"

describe Timers::Events do
	let(:events) {subject.new}
	
	it "should register an event" do
		fired = false
		
		callback = proc do |_time|
			fired = true
		end
		
		events.schedule(0.1, callback)
		
		expect(events.size).to be == 1
		
		events.fire(0.15)
		
		expect(events.size).to be == 0
		
		expect(fired).to be == true
	end
	
	it "should register events in order" do
		fired = []
		
		times = [0.95, 0.1, 0.3, 0.5, 0.4, 0.2, 0.01, 0.9]
		
		times.each do |requested_time|
			callback = proc do |_time|
				fired << requested_time
			end
			
			events.schedule(requested_time, callback)
		end
		
		events.fire(0.5)
		expect(fired).to be == times.sort.first(6)
		
		events.fire(1.0)
		expect(fired).to be == times.sort
	end
	
	it "should fire events with the time they were fired at" do
		fired_at = :not_fired
		
		callback = proc do |time|
			# The time we actually were fired at:
			fired_at = time
		end
		
		events.schedule(0.5, callback)
		
		events.fire(1.0)
		
		expect(fired_at).to be == 1.0
	end
	
	it "should flush cancelled events" do
		callback = proc{}
		
		10.times do
			handle = events.schedule(0.1, callback)
			handle.cancel!
		end
		
		expect(events.size).to be == 1
	end
end


================================================
FILE: test/timers/group/cancel.rb
================================================
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2014, by Lin Jen-Shin.
# Copyright, 2014-2016, by Tony Arcieri.
# Copyright, 2014-2025, by Samuel Williams.

require "timers/group"

describe Timers::Group do
	let(:group) {subject.new}
	
	it "can cancel a timer" do
		fired = false
		
		timer = group.after(0.1) { fired = true }
		timer.cancel
		
		group.wait
		
		expect(fired).to be == false
	end
	
	it "should be able to cancel twice" do
		fired = false
		
		timer = group.after(0.1) { fired = true }
		
		2.times do
			timer.cancel
			group.wait
		end
		
		expect(fired).to be == false
	end
	
	it "should be possble to reset after cancel" do
		fired = false
		
		timer = group.after(0.1) { fired = true }
		timer.cancel
		
		group.wait
		
		timer.reset
		
		group.wait
		
		expect(fired).to be == true
	end
	
	it "should cancel and remove one shot timers after they fire" do
		x = 0
		
		Timers::Wait.for(2) do |_remaining|
			timer = group.every(0.2) { x += 1 }
			group.after(0.1) { timer.cancel }
			
			group.wait
		end
		
		expect(group.timers).to be(:empty?)
		expect(x).to be == 0
	end
	
	with "#cancel" do
		it "should cancel all timers" do
			timers = 3.times.map do
				group.every(0.1) {}
			end
			
			expect(group.timers).not.to be(:empty?)
			
			group.cancel
			
			expect(group.timers).to be(:empty?)
		end
	end
end


================================================
FILE: test/timers/group/every.rb
================================================
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2014-2025, by Samuel Williams.
# Copyright, 2014-2016, by Tony Arcieri.
# Copyright, 2015, by Tommy Ong Gia Phu.
# Copyright, 2015, by Donovan Keme.

require "timers/group"

describe Timers::Group do
	let(:group) {subject.new}
	
	it "should fire several times" do
		result = []
		
		group.every(0.7) { result << :a }
		group.every(2.3) { result << :b }
		group.every(1.3) { result << :c }
		group.every(2.4) { result << :d }
		
		Timers::Wait.for(2.5) do |remaining|
			group.wait if group.wait_interval < remaining
		end
		
		expect(result).to be == [:a, :c, :a, :a, :b, :d]
	end
	
	it "should fire immediately and then several times later" do
		result = []
		
		group.every(0.7) { result << :a }
		group.every(2.3) { result << :b }
		group.now_and_every(1.3) { result << :c }
		group.now_and_every(2.4) { result << :d }
		
		Timers::Wait.for(2.5) do |remaining|
			group.wait if group.wait_interval < remaining
		end
		
		expect(result).to be == [:c, :d, :a, :c, :a, :a, :b, :d]
	end
end


================================================
FILE: test/timers/group/pause.rb
================================================
# frozen_string_literal: true

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

require "timers/group"

describe Timers::Group do
	let(:group) {subject.new}
	let(:interval) {0.01}
	
	def before
		@fired = false
		@timer = group.after(interval) {@fired = true}
		
		@fired2 = false
		@timer2 = group.after(interval) {@fired2 = true}
		
		super
	end
	
	it "does not fire when paused" do
		@timer.pause
		group.wait
		expect(@fired).to be == false
	end
	
	it "fires when continued after pause" do
		@timer.pause
		group.wait
		@timer.resume
		
		sleep(interval)
		group.wait
		
		expect(@fired).to be == true
	end
	
	it "can pause all timers at once" do
		group.pause
		group.wait
		
		expect(@fired).to be == false
		expect(@fired2).to be == false
	end
	
	it "can continue all timers at once" do
		group.pause
		group.wait
		group.resume
		
		sleep(interval + TIMER_QUANTUM)
		group.wait
		
		expect(@fired).to be == true
		expect(@fired2).to be == true
	end
	
	it "can fire the timer directly" do
		@timer.pause
		
		group.wait
		expect(@fired).not.to be == true
		
		@timer.resume
		expect(@fired).not.to be == true

		@timer.fire
		expect(@fired).to be == true
	end
end


================================================
FILE: test/timers/group.rb
================================================
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2012-2017, by Tony Arcieri.
# Copyright, 2012, by Jesse Cooke.
# Copyright, 2012, by Dimitrij Denissenko.
# Copyright, 2013, by Chuck Remes.
# Copyright, 2013, by Ron Evans.
# Copyright, 2013, by Sean Gregory.
# Copyright, 2013-2014, by Utenmiki.
# Copyright, 2013, by Jeremy Hinegardner.
# Copyright, 2014, by Bruno Enten.
# Copyright, 2014-2025, by Samuel Williams.
# Copyright, 2017, by Vít Ondruch.

require "timers/group"
require "timer_quantum"

describe Timers::Group do
	let(:group) {subject.new}
	
	with "#wait" do
		it "calls the wait block with nil" do
			called = false
			
			group.wait do |interval|
				expect(interval).to be_nil
				called = true
			end
			
			expect(called).to be == true
		end
		
		it "calls the wait block with an interval" do
			called = false
			fired = false
			
			group.after(0.1) { fired = true }
			
			group.wait do |interval|
				expect(interval).to be_within(TIMER_QUANTUM).of(0.1)
				called = true
				sleep 0.2
			end
			
			expect(called).to be == true
			expect(fired).to be == true
		end
		
		it "repeatedly calls the wait block if it sleeps less than the interval" do
			called = 0
			fired = false
			
			group.after(0.1) { fired = true }
			
			group.wait do |interval|
				called += 1
				sleep(0.01)
			end
			
			expect(called).to be > 1
			expect(fired).to be == true
		end
	end
	
	it "sleeps until the next timer" do
		interval = 0.1
		
		fired = false
		group.after(interval) {fired = true}
		group.wait
		
		expect(fired).to be == true
	end
	
	it "fires instantly when next timer is in the past" do
		fired = false
		group.after(TIMER_QUANTUM) { fired = true }
		sleep(TIMER_QUANTUM * 2)
		group.wait
		
		expect(fired).to be == true
	end
	
	it "calculates the interval until the next timer should fire" do
		interval = 0.1
		
		group.after(interval)
		expect(group.wait_interval).to be_within(TIMER_QUANTUM).of interval
		
		sleep(interval)
		expect(group.wait_interval).to be <= 0
	end
	
	it "fires timers in the correct order" do
		result = []
		
		group.after(TIMER_QUANTUM * 2) { result << :two }
		group.after(TIMER_QUANTUM * 3) { result << :three }
		group.after(TIMER_QUANTUM * 1) { result << :one }
		
		sleep(TIMER_QUANTUM * 4)
		group.fire
		
		expect(result).to be == [:one, :two, :three]
	end
	
	it "raises TypeError if given an invalid time" do
		expect do
			group.after(nil) { nil }
		end.to raise_exception(TypeError)
	end
	
	with "#now_and_after" do
		it "fires the timer immediately" do
			result = []
			
			group.now_and_after(TIMER_QUANTUM * 2) { result << :foo }
			
			expect(result).to be == [:foo]
		end
		
		it "fires the timer at the correct time" do
			result = []
			
			group.now_and_after(TIMER_QUANTUM * 2) { result << :foo }
			
			group.wait
			
			expect(result).to be == [:foo, :foo]
		end
	end
	
	with "recurring timers" do
		it "continues to fire the timers at each interval" do
			result = []
			
			group.every(TIMER_QUANTUM * 2) { result << :foo }
			
			sleep TIMER_QUANTUM * 3
			group.fire
			expect(result).to be == [:foo]
			
			sleep TIMER_QUANTUM * 5
			group.fire
			expect(result).to be == [:foo, :foo]
		end
	end
	
	it "calculates the proper interval to wait until firing" do
		interval_ms = 25
		
		group.after(interval_ms / 1000.0)
		
		expect(group.wait_interval).to be_within(TIMER_QUANTUM).of(interval_ms / 1000.0)
	end
	
	with "delay timer" do
		it "adds appropriate amount of time to timer" do
			timer = group.after(10)
			timer.delay(5)
			expect(timer.offset - group.current_offset).to be_within(TIMER_QUANTUM).of(15)
		end
	end
	
	with "delay timer collection" do
		it "delay on set adds appropriate amount of time to all timers" do
			timer = group.after(10)
			timer2 = group.after(20)
			group.delay(5)
			expect(timer.offset - group.current_offset).to be_within(TIMER_QUANTUM).of(15)
			expect(timer2.offset - group.current_offset).to be_within(TIMER_QUANTUM).of(25)
		end
	end
	
	with "on delaying a timer" do
		it "fires timers in the correct order" do
			result = []
			
			group.after(TIMER_QUANTUM * 2) { result << :two }
			group.after(TIMER_QUANTUM * 3) { result << :three }
			first = group.after(TIMER_QUANTUM * 1) { result << :one }
			first.delay(TIMER_QUANTUM * 3)
			
			sleep TIMER_QUANTUM * 5
			group.fire
			
			expect(result).to be == [:two, :three, :one]
		end
	end
	
	with "#inspect" do
		it "before firing" do
			fired = false
			timer = group.after(TIMER_QUANTUM * 5) { fired = true }
			timer.pause
			expect(fired).not.to be == true
			expect(timer.inspect).to be =~ /\A#<Timers::Timer:0x[\da-f]+ fires in [-\.\de]+ seconds>\Z/
		end
		
		it "after firing" do
			fired = false
			timer = group.after(TIMER_QUANTUM) { fired = true }
			
			group.wait
			
			expect(fired).to be == true
			expect(timer.inspect).to be =~/\A#<Timers::Timer:0x[\da-f]+ fired [-\.\de]+ seconds ago>\Z/
		end
		
		it "recurring firing" do
			result = []
			timer = group.every(TIMER_QUANTUM) { result << :foo }
			
			group.wait
			expect(result).to be(:any?)
			regex = /\A#<Timers::Timer:0x[\da-f]+ fires in [-\.\de]+ seconds, recurs every #{TIMER_QUANTUM}>\Z/
			expect(timer.inspect).to be =~ regex
		end
	end
	
	with "#fires_in" do
		let(:interval) {0.01}
		
		with "recurring timer" do
			let(:timer) {group.every(interval){true}}

			it "calculates the interval until the next fire if it's recurring" do
				expect(timer.fires_in).to be_within(TIMER_QUANTUM).of(interval)
			end
		end
		
		with "non-recurring timer" do
			let(:timer) {group.after(interval){true}}
			
			it "calculates the interval until the next fire if it hasn't already fired" do
				expect(timer.fires_in).to be_within(TIMER_QUANTUM).of(interval)
			end
			
			it "calculates the interval since last fire if already fired" do
				# Create the timer:
				timer
				
				group.wait
				
				sleep(TIMER_QUANTUM)
				
				expect(timer.fires_in).to be < 0.0
			end
		end
	end
end


================================================
FILE: test/timers/performance.rb
================================================
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2014-2025, by Samuel Williams.
# Copyright, 2014-2016, by Tony Arcieri.
# Copyright, 2015, by Donovan Keme.
# Copyright, 2021, by Wander Hillen.

# Event based timers:

# Serviced 31812 events in 2.39075272 seconds, 13306.320832794887 e/s.
# Thread ID: 7336700
# Fiber ID: 30106340
# Total: 2.384043
# Sort by: self_time

# %self      total      self      wait     child     calls  name
# 13.48      0.510     0.321     0.000     0.189    369133  Timers::Events::Handle#<=>
#  8.12      0.194     0.194     0.000     0.000    427278  Timers::Events::Handle#to_f
#  4.55      0.109     0.109     0.000     0.000    427278  Float#<=>
#  4.40      1.857     0.105     0.000     1.752    466376 *Timers::Events#bsearch
#  4.30      0.103     0.103     0.000     0.000    402945  Float#to_f
#  2.65      0.063     0.063     0.000     0.000     33812  Array#insert
#  2.64      1.850     0.063     0.000     1.787     33812  Timers::Events#schedule
#  2.40      1.930     0.057     0.000     1.873     33812  Timers::Timer#reset
#  1.89      1.894     0.045     0.000     1.849     31812  Timers::Timer#fire
#  1.69      1.966     0.040     0.000     1.926     31812  Timers::Events::Handle#fire
#  1.35      0.040     0.032     0.000     0.008     33812  Timers::Events::Handle#initialize
#  1.29      0.044     0.031     0.000     0.013     44451  Timers::Group#current_offset

# SortedSet based timers:

# Serviced 32516 events in 66.753277275 seconds, 487.1072288781219 e/s.
# Thread ID: 15995640
# Fiber ID: 38731780
# Total: 66.716394
# Sort by: self_time

# %self      total      self      wait     child     calls  name
# 54.73     49.718    36.513     0.000    13.205  57084873  Timers::Timer#<=>
# 23.74     65.559    15.841     0.000    49.718     32534  Array#sort!
# 19.79     13.205    13.205     0.000     0.000  57084873  Float#<=>

# Max out events performance (on my computer):
# Serviced 1142649 events in 11.194903921 seconds, 102068.70405115146 e/s.

require "timers/group"

describe Timers::Group do
	let(:group) {subject.new}
	
	with "profiler" do
		if defined? RubyProf
			def before
				# Running RubyProf makes the code slightly slower.
				RubyProf.start
				puts "*** Running with RubyProf reduces performance ***"
				
				super
			end
			
			def after
				super
				
				if RubyProf.running?
					# file = arg.metadata[:description].gsub(/\s+/, '-')
					
					result = RubyProf.stop
					
					printer = RubyProf::FlatPrinter.new(result)
					printer.print($stderr, min_percent: 1.0)
				end
			end
		end
		
		it "runs efficiently" do
			result = []
			range = (1..500)
			duration = 2.0
			
			total = 0
			range.each do |index|
				offset = index.to_f / range.max
				total += (duration / offset).floor
				
				group.every(index.to_f / range.max, :strict) { result << index }
			end
			
			group.wait while result.size < total
			
			rate = result.size.to_f / group.current_offset
			inform "Serviced #{result.size} events in #{group.current_offset} seconds, #{rate} e/s."
			
			expect(group.current_offset).to be_within(20).percent_of(duration)
		end
	end
	
	it "runs efficiently at high volume" do
		results = []
		range = (1..300)
		groups = (1..20)
		duration = 2.0
		
		timers = []
		@mutex = Mutex.new
		start = Time.now
		
		groups.each do
			timers << Thread.new do
				result = []
				timer = Timers::Group.new
				total = 0
				
				range.each do |index|
					offset = index.to_f / range.max
					total += (duration / offset).floor
					timer.every(index.to_f / range.max, :strict) { result << index }
				end
				
				timer.wait while result.size < total
				@mutex.synchronize { results += result }
			end
		end
		
		timers.each { |t| t.join }
		finish = Time.now
		
		runtime = finish - start
		rate = results.size.to_f / runtime
		
		inform "Serviced #{results.size} events in #{runtime} seconds, #{rate} e/s; across #{groups.max} timers."
		
		expect(runtime).to be_within(20).percent_of(duration)
	end
	
	it "copes with very large amounts of timers" do
		# This spec tries to emulate (as best as possible) the timer characteristics of the
		# following scenario:
		# - a fairly busy Falcon server serving a constant stream of request that spend most of their time
		#   in a long database call. Both the web request and the db call have a timeout attached
		# - there will already exist a lot of timers in the queue and more are added all the time
		# - the server is assumed to be busy so there are "always" new requests waiting to be accept()-ed
		#   and thus the server spends relatively little time actually sleeping and most of its time in
		#   either the reactor or an active fiber.
		# - On each loop of the reactor it will run any fibers in the ready queue, accept any waiting
		#   requests on the server socket and then call wait_interval to see if there are any expired
		#   timeouts that need to be handled.
		
		# Result for PriorityHeap based timer queue: Inserted 20k timers in 0.055050924 seconds
		# Result for Array based timer queue: 			 Inserted 20k timers in 0.141001845 seconds
		
		results = []
		
		# Prefill the timer queue with a lot of timers in the semidistant future
		20000.times do
			group.after(10) { results << "yay!" }
		end
		
		# add one timer which is done immediately, to get the pending array into the queue
		group.after(-1) { results << "I am first!" }
		group.wait
		expect(results.size).to be == 1
		expect(results.first).to be == "I am first!"
		
		# 20k extra requests come in and get added into the queue
		start = Time.now
		
		20000.times do
			# add new timer to the queue (later than all the others so far)
			group.after(15) { result << "yay again!" }
			# wait_interval in the reactor loop
			group.wait_interval()
		end
		
		expect(group.events.size).to be == 40_000
		inform "Inserted 20k timers in #{Time.now - start} seconds"
	end
end


================================================
FILE: test/timers/priority_heap.rb
================================================
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2021, by Wander Hillen.
# Copyright, 2021-2025, by Samuel Williams.

require "timers/priority_heap"

describe Timers::PriorityHeap do
	let(:priority_heap) {subject.new}
	
	with "empty heap" do 
		it "should return nil when the first element is requested" do
			expect(priority_heap.peek).to be_nil
		end
		
		it "should return nil when the first element is extracted" do
			expect(priority_heap.pop).to be_nil
		end
		
		it "should report its size as zero" do
			expect(priority_heap.size).to be(:zero?)
		end
	end
	
	it "returns the same element after inserting a single element" do
		priority_heap.push(1)
		expect(priority_heap.size).to be == 1
		expect(priority_heap.pop).to be == 1
		expect(priority_heap.size).to be(:zero?)
	end
	
	it "should return inserted elements in ascending order no matter the insertion order" do
		(1..10).to_a.shuffle.each do |e|
			priority_heap.push(e)
		end
		
		expect(priority_heap.size).to be == 10
		expect(priority_heap.peek).to be == 1
		
		result = []
		10.times do
			result << priority_heap.pop
		end
		
		expect(result.size).to be == 10
		expect(priority_heap.size).to be(:zero?)
		expect(result.sort).to be == result
	end

	with "maintaining the heap invariant" do
		it "for empty heaps" do
			expect(priority_heap).to be(:valid?)
		end

		it "for heap of size 1" do
			priority_heap.push(123)
			expect(priority_heap).to be(:valid?)
		end
		# Exhaustive testing of all permutations of [1..6]
		it "for all permutations of size 6" do
			[1,2,3,4,5,6].permutation do |arr|
				priority_heap.clear!
				arr.each { |e| priority_heap.push(e) }
				expect(priority_heap).to be(:valid?)
			end
		end

		# A few examples with more elements (but not ALL permutations)
		it "for larger amounts of values" do
			5.times do
				priority_heap.clear!
				(1..1000).to_a.shuffle.each { |e| priority_heap.push(e) }
				expect(priority_heap).to be(:valid?)
			end
		end

		# What if we insert several of the same item along with others?
		it "with several elements of the same value" do
			test_values = (1..10).to_a + [4] * 5
			test_values.each { |e| priority_heap.push(e) }
			expect(priority_heap).to be(:valid?)
		end
	end
end


================================================
FILE: test/timers/strict.rb
================================================
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2014-2025, by Samuel Williams.
# Copyright, 2014-2016, by Tony Arcieri.

require "timers/group"
require "timer_quantum"

describe Timers::Group do
	let(:group) {subject.new}
	
	it "should not diverge too much" do
		fired = :not_fired_yet
		count = 0
		quantum = 0.01
		
		start_offset = group.current_offset
		Timers::Timer.new(group, quantum, :strict, start_offset) do |offset|
			fired = offset
			count += 1
		end
		
		iterations = 100
		group.wait while count < iterations
		
		# In my testing on the JVM, without the :strict recurring, I noticed 60ms of error here.
		expect(fired - start_offset).to be_within(quantum + TIMER_QUANTUM).of(iterations * quantum)
	end
	
	it "should only fire 0-interval timer once per iteration" do
		count = 0
		
		start_offset = group.current_offset
		Timers::Timer.new(group, 0, :strict, start_offset) do |offset, timer|
			count += 1
		end
		
		group.wait
		
		expect(count).to be == 1
	end
end


================================================
FILE: test/timers/timer.rb
================================================
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2025, by Patrik Wenger.
# Copyright, 2025, by Samuel Williams.

require "timers/timer"

describe Timers::Timer do
	let(:group) {Timers::Group.new}
	
	it "should return the block value when fired" do
		timer  = group.after(10) {:foo}
		result = timer.fire

		expect(result).to be == :foo
	end
end


================================================
FILE: test/timers/wait.rb
================================================
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2014-2025, by Samuel Williams.
# Copyright, 2014-2016, by Tony Arcieri.

require "timers/wait"
require "timer_quantum"

describe Timers::Wait do
	let(:interval) {0.1}
	let(:repeats) {10}
	
	it "repeats until timeout expired" do
		timeout = Timers::Wait.new(interval*repeats)
		count = 0
		previous_remaining = nil
		
		timeout.while_time_remaining do |remaining|
			if previous_remaining
				expect(remaining).to be_within(TIMER_QUANTUM).of(previous_remaining - interval)
			end
			
			previous_remaining = remaining
			
			count += 1
			sleep(interval)
		end
		
		expect(count).to be == repeats
	end
	
	it "yields results as soon as possible" do
		timeout = Timers::Wait.new(5)
		
		result = timeout.while_time_remaining do |_remaining|
			break :done
		end
		
		expect(result).to be == :done
	end
	
	with "#for" do
		with "no duration" do
			it "waits forever" do
				count = 0
				Timers::Wait.for(nil) do
					count += 1
					break if count > 10
				end
				
				expect(count).to be > 10
			end
		end
	end
end


================================================
FILE: timers.gemspec
================================================
# frozen_string_literal: true

require_relative "lib/timers/version"

Gem::Specification.new do |spec|
	spec.name = "timers"
	spec.version = Timers::VERSION
	
	spec.summary = "Pure Ruby one-shot and periodic timers."
	spec.authors = ["Tony Arcieri", "Samuel Williams", "Donovan Keme", "Wander Hillen", "Utenmiki", "Jeremy Hinegardner", "Sean Gregory", "Chuck Remes", "Olle Jonsson", "Ron Evans", "Tommy Ong Gia Phu", "Larry Lv", "Lin Jen-Shin", "Ryunosuke Sato", "Atul Bhosale", "Bruno Enten", "Dimitrij Denissenko", "Jesse Cooke", "Klaus Trainer", "Lavir the Whiolet", "Mike Bourgeous", "Nicholas Evans", "Patrik Wenger", "Peter Goldstein", "Ryan LeCompte", "Tim Smith", "Vít Ondruch", "Will Jessop", "Yoshiki Takagi"]
	spec.license = "MIT"
	
	spec.cert_chain  = ["release.cert"]
	spec.signing_key = File.expand_path("~/.gem/release.pem")
	
	spec.homepage = "https://github.com/socketry/timers"
	
	spec.metadata = {
		"source_code_uri" => "https://github.com/socketry/timers.git",
	}
	
	spec.files = Dir.glob(["{lib}/**/*", "*.md"], File::FNM_DOTMATCH, base: __dir__)
	
	spec.required_ruby_version = ">= 3.1"
end
Download .txt
gitextract_wr79o4sy/

├── .editorconfig
├── .git-blame-ignore-revs
├── .github/
│   └── workflows/
│       ├── documentation-coverage.yaml
│       ├── documentation.yaml
│       ├── rubocop.yaml
│       ├── test-coverage.yaml
│       └── test.yaml
├── .gitignore
├── .mailmap
├── .rubocop.yml
├── config/
│   └── sus.rb
├── fixtures/
│   └── timer_quantum.rb
├── gems.rb
├── lib/
│   ├── timers/
│   │   ├── events.rb
│   │   ├── group.rb
│   │   ├── interval.rb
│   │   ├── priority_heap.rb
│   │   ├── timer.rb
│   │   ├── version.rb
│   │   └── wait.rb
│   └── timers.rb
├── license.md
├── readme.md
├── release.cert
├── test/
│   └── timers/
│       ├── events.rb
│       ├── group/
│       │   ├── cancel.rb
│       │   ├── every.rb
│       │   └── pause.rb
│       ├── group.rb
│       ├── performance.rb
│       ├── priority_heap.rb
│       ├── strict.rb
│       ├── timer.rb
│       └── wait.rb
└── timers.gemspec
Download .txt
SYMBOL INDEX (77 symbols across 10 files)

FILE: fixtures/timer_quantum.rb
  class TimerQuantum (line 6) | class TimerQuantum
    method resolve (line 7) | def self.resolve
    method to_f (line 11) | def to_f
    method precision (line 17) | def precision
    method measure_host_precision (line 21) | def measure_host_precision(repeats: 100, duration: 0.01)
    method now (line 41) | def now

FILE: lib/timers/events.rb
  type Timers (line 14) | module Timers
    class Events (line 16) | class Events
      class Handle (line 18) | class Handle
        method initialize (line 21) | def initialize(time, callback)
        method cancel! (line 30) | def cancel!
        method cancelled? (line 37) | def cancelled?
        method <=> (line 41) | def <=> other
        method fire (line 46) | def fire(time)
      method initialize (line 51) | def initialize
      method schedule (line 59) | def schedule(time, callback)
      method first (line 70) | def first
      method size (line 80) | def size
      method fire (line 85) | def fire(time)
      method merge! (line 97) | def merge!
      method flush! (line 105) | def flush!

FILE: lib/timers/group.rb
  type Timers (line 16) | module Timers
    class Group (line 18) | class Group
      method initialize (line 24) | def initialize
      method after (line 45) | def after(interval, &block)
      method now_and_after (line 51) | def now_and_after(interval, &block)
      method every (line 58) | def every(interval, recur = true, &block)
      method now_and_every (line 64) | def now_and_every(interval, recur = true, &block)
      method wait (line 72) | def wait
      method wait_interval (line 94) | def wait_interval(offset = current_offset)
      method fire (line 101) | def fire(offset = current_offset)
      method pause (line 106) | def pause
      method resume (line 111) | def resume
      method delay (line 118) | def delay(seconds)
      method cancel (line 125) | def cancel
      method current_offset (line 130) | def current_offset

FILE: lib/timers/interval.rb
  type Timers (line 6) | module Timers
    class Interval (line 8) | class Interval
      method initialize (line 10) | def initialize
      method start (line 15) | def start
      method stop (line 21) | def stop
      method to_f (line 29) | def to_f
      method duration (line 33) | def duration
      method now (line 37) | def now

FILE: lib/timers/priority_heap.rb
  type Timers (line 7) | module Timers
    class PriorityHeap (line 12) | class PriorityHeap
      method initialize (line 13) | def initialize
      method peek (line 21) | def peek
      method size (line 26) | def size
      method pop (line 32) | def pop
      method push (line 61) | def push(element)
      method clear! (line 74) | def clear!
      method valid? (line 80) | def valid?
      method bubble_up (line 92) | def bubble_up(index)
      method bubble_down (line 108) | def bubble_down(index)

FILE: lib/timers/timer.rb
  type Timers (line 11) | module Timers
    class Timer (line 17) | class Timer
      method initialize (line 21) | def initialize(group, interval, recurring = false, offset = nil, &bl...
      method paused? (line 34) | def paused?
      method pause (line 38) | def pause
      method resume (line 48) | def resume
      method delay (line 60) | def delay(seconds)
      method cancel (line 69) | def cancel
      method reset (line 81) | def reset(offset = @group.current_offset)
      method fire (line 96) | def fire(offset = @group.current_offset)
      method fires_in (line 114) | def fires_in
      method inspect (line 119) | def inspect

FILE: lib/timers/version.rb
  type Timers (line 8) | module Timers

FILE: lib/timers/wait.rb
  type Timers (line 11) | module Timers
    class Wait (line 13) | class Wait
      method for (line 14) | def self.for(duration, &block)
      method initialize (line 27) | def initialize(duration)
      method while_time_remaining (line 36) | def while_time_remaining
      method time_remaining? (line 48) | def time_remaining?

FILE: test/timers/group/pause.rb
  function before (line 12) | def before

FILE: test/timers/performance.rb
  function before (line 54) | def before
  function after (line 62) | def after
Condensed preview — 35 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (60K chars).
[
  {
    "path": ".editorconfig",
    "chars": 105,
    "preview": "root = true\n\n[*]\nindent_style = tab\nindent_size = 2\n\n[*.{yml,yaml}]\nindent_style = space\nindent_size = 2\n"
  },
  {
    "path": ".git-blame-ignore-revs",
    "chars": 41,
    "preview": "9180068e70c5b5c1fdb9a6c47f4d8f2553fc7104\n"
  },
  {
    "path": ".github/workflows/documentation-coverage.yaml",
    "chars": 450,
    "preview": "name: Documentation Coverage\n\non: [push, pull_request]\n\npermissions:\n  contents: read\n\nenv:\n  CONSOLE_OUTPUT: XTerm\n  CO"
  },
  {
    "path": ".github/workflows/documentation.yaml",
    "chars": 1138,
    "preview": "name: Documentation\n\non:\n  push:\n    branches:\n      - main\n\n# Sets permissions of the GITHUB_TOKEN to allow deployment "
  },
  {
    "path": ".github/workflows/rubocop.yaml",
    "chars": 376,
    "preview": "name: RuboCop\n\non: [push, pull_request]\n\npermissions:\n  contents: read\n\nenv:\n  CONSOLE_OUTPUT: XTerm\n\njobs:\n  check:\n   "
  },
  {
    "path": ".github/workflows/test-coverage.yaml",
    "chars": 1181,
    "preview": "name: Test Coverage\n\non: [push, pull_request]\n\npermissions:\n  contents: read\n\nenv:\n  CONSOLE_OUTPUT: XTerm\n  COVERAGE: P"
  },
  {
    "path": ".github/workflows/test.yaml",
    "chars": 966,
    "preview": "name: Test\n\non: [push, pull_request]\n\npermissions:\n  contents: read\n\nenv:\n  CONSOLE_OUTPUT: XTerm\n\njobs:\n  test:\n    nam"
  },
  {
    "path": ".gitignore",
    "chars": 52,
    "preview": "/.bundle/\n/pkg/\n/gems.locked\n/.covered.db\n/external\n"
  },
  {
    "path": ".mailmap",
    "chars": 381,
    "preview": "Nicholas Evans <nevans@410labs.com>\nRon Evans <ron.evans@gmail.com>\nSean Gregory <skinnyjames@pigadmirersclub.net>\nUtenm"
  },
  {
    "path": ".rubocop.yml",
    "chars": 858,
    "preview": "AllCops:\n  DisabledByDefault: true\n\nLayout/IndentationStyle:\n  Enabled: true\n  EnforcedStyle: tabs\n\nLayout/InitialIndent"
  },
  {
    "path": "config/sus.rb",
    "chars": 153,
    "preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2022-2025, by Samuel Williams.\n\nrequire \"c"
  },
  {
    "path": "fixtures/timer_quantum.rb",
    "chars": 943,
    "preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2022-2025, by Samuel Williams.\n\nclass Time"
  },
  {
    "path": "gems.rb",
    "chars": 483,
    "preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2012-2016, by Tony Arcieri.\n# Copyright, 2"
  },
  {
    "path": "lib/timers/events.rb",
    "chars": 2453,
    "preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2022, by Samuel Williams.\n# Copyright"
  },
  {
    "path": "lib/timers/group.rb",
    "chars": 3212,
    "preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2025, by Samuel Williams.\n# Copyright"
  },
  {
    "path": "lib/timers/interval.rb",
    "chars": 646,
    "preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2018-2022, by Samuel Williams.\n\nmodule Tim"
  },
  {
    "path": "lib/timers/priority_heap.rb",
    "chars": 4221,
    "preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2021, by Wander Hillen.\n# Copyright, 2021-"
  },
  {
    "path": "lib/timers/timer.rb",
    "chars": 3244,
    "preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2025, by Samuel Williams.\n# Copyright"
  },
  {
    "path": "lib/timers/version.rb",
    "chars": 224,
    "preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2012-2016, by Tony Arcieri.\n# Copyright, 2"
  },
  {
    "path": "lib/timers/wait.rb",
    "chars": 1036,
    "preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2025, by Samuel Williams.\n# Copyright"
  },
  {
    "path": "lib/timers.rb",
    "chars": 650,
    "preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2012-2016, by Tony Arcieri.\n# Copyright, 2"
  },
  {
    "path": "license.md",
    "chars": 2130,
    "preview": "# MIT License\n\nCopyright, 2012-2017, by Tony Arcieri.  \nCopyright, 2012, by Ryan LeCompte.  \nCopyright, 2012, by Jesse C"
  },
  {
    "path": "readme.md",
    "chars": 3268,
    "preview": "# Timers\n\nCollections of one-shot and periodic timers, intended for use with event loops such as [async](https://github."
  },
  {
    "path": "release.cert",
    "chars": 1740,
    "preview": "-----BEGIN CERTIFICATE-----\nMIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11\nZWwud2lsbGlhbXMxHTAbBgoJkia"
  },
  {
    "path": "test/timers/events.rb",
    "chars": 1376,
    "preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2025, by Samuel Williams.\n# Copyright"
  },
  {
    "path": "test/timers/group/cancel.rb",
    "chars": 1364,
    "preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014, by Lin Jen-Shin.\n# Copyright, 2014-2"
  },
  {
    "path": "test/timers/group/every.rb",
    "chars": 1068,
    "preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2025, by Samuel Williams.\n# Copyright"
  },
  {
    "path": "test/timers/group/pause.rb",
    "chars": 1201,
    "preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2022-2025, by Samuel Williams.\n\nrequire \"t"
  },
  {
    "path": "test/timers/group.rb",
    "chars": 5963,
    "preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2012-2017, by Tony Arcieri.\n# Copyright, 2"
  },
  {
    "path": "test/timers/performance.rb",
    "chars": 5918,
    "preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2025, by Samuel Williams.\n# Copyright"
  },
  {
    "path": "test/timers/priority_heap.rb",
    "chars": 2240,
    "preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2021, by Wander Hillen.\n# Copyright, 2021-"
  },
  {
    "path": "test/timers/strict.rb",
    "chars": 1012,
    "preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2025, by Samuel Williams.\n# Copyright"
  },
  {
    "path": "test/timers/timer.rb",
    "chars": 374,
    "preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2025, by Patrik Wenger.\n# Copyright, 2025,"
  },
  {
    "path": "test/timers/wait.rb",
    "chars": 1092,
    "preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2025, by Samuel Williams.\n# Copyright"
  },
  {
    "path": "timers.gemspec",
    "chars": 1114,
    "preview": "# frozen_string_literal: true\n\nrequire_relative \"lib/timers/version\"\n\nGem::Specification.new do |spec|\n\tspec.name = \"tim"
  }
]

About this extraction

This page contains the full source code of the socketry/timers GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 35 files (51.4 KB), approximately 16.7k tokens, and a symbol index with 77 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!