[
  {
    "path": ".editorconfig",
    "content": "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",
    "content": "9180068e70c5b5c1fdb9a6c47f4d8f2553fc7104\n"
  },
  {
    "path": ".github/workflows/documentation-coverage.yaml",
    "content": "name: Documentation Coverage\n\non: [push, pull_request]\n\npermissions:\n  contents: read\n\nenv:\n  CONSOLE_OUTPUT: XTerm\n  COVERAGE: PartialSummary\n\njobs:\n  validate:\n    runs-on: ubuntu-latest\n    \n    steps:\n    - uses: actions/checkout@v4\n    - uses: ruby/setup-ruby@v1\n      with:\n        ruby-version: \"3.4\"\n        bundler-cache: true\n    \n    - name: Validate coverage\n      timeout-minutes: 5\n      run: bundle exec bake decode:index:coverage lib\n"
  },
  {
    "path": ".github/workflows/documentation.yaml",
    "content": "name: Documentation\n\non:\n  push:\n    branches:\n      - main\n\n# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages:\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\n# Allow one concurrent deployment:\nconcurrency:\n  group: \"pages\"\n  cancel-in-progress: true\n\nenv:\n  CONSOLE_OUTPUT: XTerm\n  BUNDLE_WITH: maintenance\n\njobs:\n  generate:\n    runs-on: ubuntu-latest\n    \n    steps:\n    - uses: actions/checkout@v4\n\n    - uses: ruby/setup-ruby@v1\n      with:\n        ruby-version: \"3.4\"\n        bundler-cache: true\n    \n    - name: Installing packages\n      run: sudo apt-get install wget\n    \n    - name: Generate documentation\n      timeout-minutes: 5\n      run: bundle exec bake utopia:project:static --force no\n    \n    - name: Upload documentation artifact\n      uses: actions/upload-pages-artifact@v3\n      with:\n        path: docs\n  \n  deploy:\n    runs-on: ubuntu-latest\n    \n    environment:\n      name: github-pages\n      url: ${{steps.deployment.outputs.page_url}}\n    \n    needs: generate\n    steps:\n      - name: Deploy to GitHub Pages\n        id: deployment\n        uses: actions/deploy-pages@v4\n"
  },
  {
    "path": ".github/workflows/rubocop.yaml",
    "content": "name: RuboCop\n\non: [push, pull_request]\n\npermissions:\n  contents: read\n\nenv:\n  CONSOLE_OUTPUT: XTerm\n\njobs:\n  check:\n    runs-on: ubuntu-latest\n    \n    steps:\n    - uses: actions/checkout@v4\n    - uses: ruby/setup-ruby@v1\n      with:\n        ruby-version: ruby\n        bundler-cache: true\n    \n    - name: Run RuboCop\n      timeout-minutes: 10\n      run: bundle exec rubocop\n"
  },
  {
    "path": ".github/workflows/test-coverage.yaml",
    "content": "name: Test Coverage\n\non: [push, pull_request]\n\npermissions:\n  contents: read\n\nenv:\n  CONSOLE_OUTPUT: XTerm\n  COVERAGE: PartialSummary\n\njobs:\n  test:\n    name: ${{matrix.ruby}} on ${{matrix.os}}\n    runs-on: ${{matrix.os}}-latest\n    \n    strategy:\n      matrix:\n        os:\n          - ubuntu\n          - macos\n        \n        ruby:\n          - \"3.4\"\n    \n    steps:\n    - uses: actions/checkout@v4\n    - uses: ruby/setup-ruby@v1\n      with:\n        ruby-version: ${{matrix.ruby}}\n        bundler-cache: true\n    \n    - name: Run tests\n      timeout-minutes: 5\n      run: bundle exec bake test\n    \n    - uses: actions/upload-artifact@v4\n      with:\n        include-hidden-files: true\n        if-no-files-found: error\n        name: coverage-${{matrix.os}}-${{matrix.ruby}}\n        path: .covered.db\n  \n  validate:\n    needs: test\n    runs-on: ubuntu-latest\n    \n    steps:\n    - uses: actions/checkout@v4\n    - uses: ruby/setup-ruby@v1\n      with:\n        ruby-version: \"3.4\"\n        bundler-cache: true\n    \n    - uses: actions/download-artifact@v4\n    \n    - name: Validate coverage\n      timeout-minutes: 5\n      run: bundle exec bake covered:validate --paths */.covered.db \\;\n"
  },
  {
    "path": ".github/workflows/test.yaml",
    "content": "name: Test\n\non: [push, pull_request]\n\npermissions:\n  contents: read\n\nenv:\n  CONSOLE_OUTPUT: XTerm\n\njobs:\n  test:\n    name: ${{matrix.ruby}} on ${{matrix.os}}\n    runs-on: ${{matrix.os}}-latest\n    continue-on-error: ${{matrix.experimental}}\n    \n    strategy:\n      matrix:\n        os:\n          - ubuntu\n          - macos\n        \n        ruby:\n          - \"3.1\"\n          - \"3.2\"\n          - \"3.3\"\n          - \"3.4\"\n        \n        experimental: [false]\n        \n        include:\n          - os: ubuntu\n            ruby: truffleruby\n            experimental: true\n          - os: ubuntu\n            ruby: jruby\n            experimental: true\n          - os: ubuntu\n            ruby: head\n            experimental: true\n    \n    steps:\n    - uses: actions/checkout@v4\n    - uses: ruby/setup-ruby@v1\n      with:\n        ruby-version: ${{matrix.ruby}}\n        bundler-cache: true\n    \n    - name: Run tests\n      timeout-minutes: 10\n      run: bundle exec bake test\n"
  },
  {
    "path": ".gitignore",
    "content": "/.bundle/\n/pkg/\n/gems.locked\n/.covered.db\n/external\n"
  },
  {
    "path": ".mailmap",
    "content": "Nicholas Evans <nevans@410labs.com>\nRon Evans <ron.evans@gmail.com>\nSean Gregory <skinnyjames@pigadmirersclub.net>\nUtenmiki <utenmiki@gmail.com>\nDonovan Keme <code@extremist.digital>\nDonovan Keme <de@freed.network>\nDonovan Keme <digitalextremist@users.noreply.github.com>\nUtenmiki <takiy33@gmail.com>\nTommy Ong Gia Phu <tommyogp@gmail.com>\nRyunosuke Sato <tricknotes.rs@gmail.com>\n"
  },
  {
    "path": ".rubocop.yml",
    "content": "AllCops:\n  DisabledByDefault: true\n\nLayout/IndentationStyle:\n  Enabled: true\n  EnforcedStyle: tabs\n\nLayout/InitialIndentation:\n  Enabled: true\n\nLayout/IndentationWidth:\n  Enabled: true\n  Width: 1\n\nLayout/IndentationConsistency:\n  Enabled: true\n  EnforcedStyle: normal\n\nLayout/BlockAlignment:\n  Enabled: true\n\nLayout/EndAlignment:\n  Enabled: true\n  EnforcedStyleAlignWith: start_of_line\n\nLayout/BeginEndAlignment:\n  Enabled: true\n  EnforcedStyleAlignWith: start_of_line\n\nLayout/ElseAlignment:\n  Enabled: true\n\nLayout/DefEndAlignment:\n  Enabled: true\n\nLayout/CaseIndentation:\n  Enabled: true\n\nLayout/CommentIndentation:\n  Enabled: true\n\nLayout/EmptyLinesAroundClassBody:\n  Enabled: true\n\nLayout/EmptyLinesAroundModuleBody:\n  Enabled: true\n\nStyle/FrozenStringLiteralComment:\n  Enabled: true\n\nStyle/StringLiterals:\n  Enabled: true\n  EnforcedStyle: double_quotes\n"
  },
  {
    "path": "config/sus.rb",
    "content": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2022-2025, by Samuel Williams.\n\nrequire \"covered/sus\"\ninclude Covered::Sus\n"
  },
  {
    "path": "fixtures/timer_quantum.rb",
    "content": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2022-2025, by Samuel Williams.\n\nclass TimerQuantum\n\tdef self.resolve\n\t\tself.new.to_f\n\tend\n\t\n\tdef to_f\n\t\tprecision\n\tend\n\t\n\tprivate\n\t\n\tdef precision\n\t\t@precision ||= self.measure_host_precision\n\tend\n\t\n\tdef measure_host_precision(repeats: 100, duration: 0.01)\n\t\t# Measure the precision sleep using the monotonic clock:\n\t\tstart_time = self.now\n\t\trepeats.times do\n\t\t\tsleep(duration)\n\t\tend\n\t\tend_time = self.now\n\t\t\n\t\tactual_duration = end_time - start_time\n\t\texpected_duration = repeats * duration\n\t\t\n\t\tif actual_duration < expected_duration\n\t\t\twarn \"Invalid precision measurement: #{actual_duration} < #{expected_duration}\"\n\t\t\treturn 0.1\n\t\tend\n\t\t\n\t\t# This computes the overhead of sleep, called `repeats` times:\n\t\treturn actual_duration - expected_duration\n\tend\n\t\n\tdef now\n\t\tProcess.clock_gettime(Process::CLOCK_MONOTONIC)\n\tend\nend\n\nTIMER_QUANTUM = TimerQuantum.resolve\n"
  },
  {
    "path": "gems.rb",
    "content": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2012-2016, by Tony Arcieri.\n# Copyright, 2014-2025, by Samuel Williams.\n# Copyright, 2015, by Donovan Keme.\n\nsource \"https://rubygems.org\"\n\ngemspec\n\ngroup :maintenance, optional: true do\n\tgem \"bake-modernize\"\n\tgem \"bake-gem\"\nend\n\ngroup :test do\n\tgem \"sus\"\n\tgem \"covered\"\n\tgem \"decode\"\n\tgem \"rubocop\"\n\t\n\tgem \"bake-test\"\n\tgem \"bake-test-external\"\n\t\n\tgem \"benchmark-ips\"\n\tgem \"ruby-prof\", platform: :mri\nend\n"
  },
  {
    "path": "lib/timers/events.rb",
    "content": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2022, by Samuel Williams.\n# Copyright, 2014-2016, by Tony Arcieri.\n# Copyright, 2014, by Lavir the Whiolet.\n# Copyright, 2015, by Utenmiki.\n# Copyright, 2015, by Donovan Keme.\n# Copyright, 2021, by Wander Hillen.\n\nrequire_relative \"timer\"\nrequire_relative \"priority_heap\"\n\nmodule Timers\n\t# Maintains a PriorityHeap of events ordered on time, which can be cancelled.\n\tclass Events\n\t\t# Represents a cancellable handle for a specific timer event.\n\t\tclass Handle\n\t\t\tinclude Comparable\n\t\t\t\n\t\t\tdef initialize(time, callback)\n\t\t\t\t@time = time\n\t\t\t\t@callback = callback\n\t\t\tend\n\t\t\t\n\t\t\t# The absolute time that the handle should be fired at.\n\t\t\tattr_reader :time\n\t\t\t\n\t\t\t# Cancel this timer, O(1).\n\t\t\tdef cancel!\n\t\t\t\t# The simplest way to keep track of cancelled status is to nullify the\n\t\t\t\t# callback. This should also be optimal for garbage collection.\n\t\t\t\t@callback = nil\n\t\t\tend\n\t\t\t\n\t\t\t# Has this timer been cancelled? Cancelled timer's don't fire.\n\t\t\tdef cancelled?\n\t\t\t\t@callback.nil?\n\t\t\tend\n\t\t\t\n\t\t\tdef <=> other\n\t\t\t\t@time <=> other.time\n\t\t\tend\n\t\t\t\n\t\t\t# Fire the callback if not cancelled with the given time parameter.\n\t\t\tdef fire(time)\n\t\t\t\t@callback.call(time) if @callback\n\t\t\tend\n\t\tend\n\t\t\n\t\tdef initialize\n\t\t\t# A sequence of handles, maintained in sorted order, future to present.\n\t\t\t# @sequence.last is the next event to be fired.\n\t\t\t@sequence = PriorityHeap.new\n\t\t\t@queue = []\n\t\tend\n\t\t\n\t\t# Add an event at the given time.\n\t\tdef schedule(time, callback)\n\t\t\tflush!\n\t\t\t\n\t\t\thandle = Handle.new(time.to_f, callback)\n\t\t\t\n\t\t\t@queue << handle\n\t\t\t\n\t\t\treturn handle\n\t\tend\n\t\t\n\t\t# Returns the first non-cancelled handle.\n\t\tdef first\n\t\t\tmerge!\n\t\t\t\n\t\t\twhile (handle = @sequence.peek)\n\t\t\t\treturn handle unless handle.cancelled?\n\t\t\t\t@sequence.pop\n\t\t\tend\n\t\tend\n\t\t\n\t\t# Returns the number of pending (possibly cancelled) events.\n\t\tdef size\n\t\t\t@sequence.size + @queue.size\n\t\tend\n\t\t\n\t\t# Fire all handles for which Handle#time is less than the given time.\n\t\tdef fire(time)\n\t\t\tmerge!\n\t\t\t\n\t\t\twhile handle = @sequence.peek and handle.time <= time\n\t\t\t\t@sequence.pop\n\t\t\t\thandle.fire(time)\n\t\t\tend\n\t\tend\n\t\t\n\t\tprivate\n\t\t\n\t\t# Move all non-cancelled timers from the pending queue to the priority heap\n\t\tdef merge!\n\t\t\twhile handle = @queue.pop\n\t\t\t\tnext if handle.cancelled?\n\t\t\t\t\n\t\t\t\t@sequence.push(handle)\n\t\t\tend\n\t\tend\n\t\t\n\t\tdef flush!\n\t\t\twhile @queue.last&.cancelled?\n\t\t\t\t@queue.pop\n\t\t\tend\n\t\tend\n\tend\nend\n"
  },
  {
    "path": "lib/timers/group.rb",
    "content": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2025, by Samuel Williams.\n# Copyright, 2014-2016, by Tony Arcieri.\n# Copyright, 2015, by Donovan Keme.\n# Copyright, 2015, by Tommy Ong Gia Phu.\n\nrequire \"set\"\nrequire \"forwardable\"\n\nrequire_relative \"interval\"\nrequire_relative \"timer\"\nrequire_relative \"events\"\n\nmodule Timers\n\t# A collection of timers which may fire at different times\n\tclass Group\n\t\tinclude Enumerable\n\t\t\n\t\textend Forwardable\n\t\tdef_delegators :@timers, :each, :empty?\n\t\t\n\t\tdef initialize\n\t\t\t@events = Events.new\n\t\t\t\n\t\t\t@timers = Set.new\n\t\t\t@paused_timers = Set.new\n\t\t\t\n\t\t\t@interval = Interval.new\n\t\t\t@interval.start\n\t\tend\n\t\t\n\t\t# Scheduled events:\n\t\tattr_reader :events\n\t\t\n\t\t# Active timers:\n\t\tattr_reader :timers\n\t\t\n\t\t# Paused timers:\n\t\tattr_reader :paused_timers\n\t\t\n\t\t# Call the given block after the given interval. The first argument will be\n\t\t# the time at which the group was asked to fire timers for.\n\t\tdef after(interval, &block)\n\t\t\tTimer.new(self, interval, false, &block)\n\t\tend\n\t\t\n\t\t# Call the given block immediately, and then after the given interval. The first\n\t\t# argument will be the time at which the group was asked to fire timers for.\n\t\tdef now_and_after(interval, &block)\n\t\t\tyield\n\t\t\tafter(interval, &block)\n\t\tend\n\t\t\n\t\t# Call the given block periodically at the given interval. The first\n\t\t# argument will be the time at which the group was asked to fire timers for.\n\t\tdef every(interval, recur = true, &block)\n\t\t\tTimer.new(self, interval, recur, &block)\n\t\tend\n\t\t\n\t\t# Call the given block immediately, and then periodically at the given interval. The first\n\t\t# argument will be the time at which the group was asked to fire timers for.\n\t\tdef now_and_every(interval, recur = true, &block)\n\t\t\tyield\n\t\t\tevery(interval, recur, &block)\n\t\tend\n\t\t\n\t\t# Wait for the next timer and fire it. Can take a block, which should behave\n\t\t# like sleep(n), except that n may be nil (sleep forever) or a negative\n\t\t# number (fire immediately after return).\n\t\tdef wait\n\t\t\tif block_given?\n\t\t\t\tyield wait_interval\n\t\t\t\t\n\t\t\t\twhile (interval = wait_interval) && interval > 0\n\t\t\t\t\tyield interval\n\t\t\t\tend\n\t\t\telse\n\t\t\t\twhile (interval = wait_interval) && interval > 0\n\t\t\t\t\t# We cannot assume that sleep will wait for the specified time, it might be +/- a bit.\n\t\t\t\t\tsleep interval\n\t\t\t\tend\n\t\t\tend\n\t\t\t\n\t\t\tfire\n\t\tend\n\t\t\n\t\t# Interval to wait until when the next timer will fire.\n\t\t# - nil: no timers\n\t\t# - -ve: timers expired already\n\t\t# -   0: timers ready to fire\n\t\t# - +ve: timers waiting to fire\n\t\tdef wait_interval(offset = current_offset)\n\t\t\tif handle = @events.first\n\t\t\t\thandle.time - Float(offset)\n\t\t\tend\n\t\tend\n\t\t\n\t\t# Fire all timers that are ready.\n\t\tdef fire(offset = current_offset)\n\t\t\t@events.fire(offset)\n\t\tend\n\t\t\n\t\t# Pause all timers.\n\t\tdef pause\n\t\t\t@timers.dup.each(&:pause)\n\t\tend\n\t\t\n\t\t# Resume all timers.\n\t\tdef resume\n\t\t\t@paused_timers.dup.each(&:resume)\n\t\tend\n\t\t\n\t\talias continue resume\n\t\t\n\t\t# Delay all timers.\n\t\tdef delay(seconds)\n\t\t\t@timers.each do |timer|\n\t\t\t\ttimer.delay(seconds)\n\t\t\tend\n\t\tend\n\t\t\n\t\t# Cancel all timers.\n\t\tdef cancel\n\t\t\t@timers.dup.each(&:cancel)\n\t\tend\n\t\t\n\t\t# The group's current time.\n\t\tdef current_offset\n\t\t\t@interval.to_f\n\t\tend\n\tend\nend\n"
  },
  {
    "path": "lib/timers/interval.rb",
    "content": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2018-2022, by Samuel Williams.\n\nmodule Timers\n\t# A collection of timers which may fire at different times\n\tclass Interval\n\t\t# Get the current elapsed monotonic time.\n\t\tdef initialize\n\t\t\t@total = 0.0\n\t\t\t@current = nil\n\t\tend\n\t\t\n\t\tdef start\n\t\t\treturn if @current\n\t\t\t\n\t\t\t@current = now\n\t\tend\n\t\t\n\t\tdef stop\n\t\t\treturn unless @current\n\t\t\t\n\t\t\t@total += duration\n\t\t\t\n\t\t\t@current = nil\n\t\tend\n\t\t\n\t\tdef to_f\n\t\t\t@total + duration\n\t\tend\n\t\t\n\t\tprotected def duration\n\t\t\tnow - @current\n\t\tend\n\t\t\n\t\tprotected def now\n\t\t\t::Process.clock_gettime(::Process::CLOCK_MONOTONIC)\n\t\tend\n\tend\nend\n"
  },
  {
    "path": "lib/timers/priority_heap.rb",
    "content": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2021, by Wander Hillen.\n# Copyright, 2021-2025, by Samuel Williams.\n\nmodule Timers\n\t# A priority queue implementation using a standard binary minheap. It uses straight comparison\n\t# of its contents to determine priority. This works because a Handle from Timers::Events implements\n\t# the '<' operation by comparing the expiry time.\n\t# See <https://en.wikipedia.org/wiki/Binary_heap> for explanations of the main methods.\n\tclass PriorityHeap\n\t\tdef initialize\n\t\t\t# The heap is represented with an array containing a binary tree. See\n\t\t\t# https://en.wikipedia.org/wiki/Binary_heap#Heap_implementation for how this array\n\t\t\t# is built up.\n\t\t\t@contents = []\n\t\tend\n\t\t\n\t\t# Returns the earliest timer or nil if the heap is empty.\n\t\tdef peek\n\t\t\t@contents[0]\n\t\tend\n\t\t\n\t\t# Returns the number of elements in the heap\n\t\tdef size\n\t\t\t@contents.size\n\t\tend\n\t\t\n\t\t# Returns the earliest timer if the heap is non-empty and removes it from the heap.\n\t\t# Returns nil if the heap is empty. (and doesn't change the heap in that case)\n\t\tdef pop\n\t\t\t# If the heap is empty:\n\t\t\tif @contents.empty?\n\t\t\t\treturn nil\n\t\t\tend\n\t\t\t\n\t\t\t# If we have only one item, no swapping is required:\n\t\t\tif @contents.size == 1\n\t\t\t\treturn @contents.pop\n\t\t\tend\n\t\t\t\n\t\t\t# Take the root of the tree:\n\t\t\tvalue = @contents[0]\n\t\t\t\n\t\t\t# Remove the last item in the tree:\n\t\t\tlast = @contents.pop\n\t\t\t\n\t\t\t# Overwrite the root of the tree with the item:\n\t\t\t@contents[0] = last\n\t\t\t\n\t\t\t# Bubble it down into place:\n\t\t\tbubble_down(0)\n\t\t\t\n\t\t\t# validate!\n\t\t\t\n\t\t\treturn value\n\t\tend\n\t\t\n\t\t# Inserts a new timer into the heap, then rearranges elements until the heap invariant is true again.\n\t\tdef push(element)\n\t\t\t# Insert the item at the end of the heap:\n\t\t\t@contents.push(element)\n\t\t\t\n\t\t\t# Bubble it up into position:\n\t\t\tbubble_up(@contents.size - 1)\n\t\t\t\n\t\t\t# validate!\n\t\t\t\n\t\t\treturn self\n\t\tend\n\t\t\n\t\t# Empties out the heap, discarding all elements\n\t\tdef clear!\n\t\t\t@contents = []\n\t\tend\n\n\t\t# Validate the heap invariant. Every element except the root must not be smaller than\n\t\t# its parent element. Note that it MAY be equal.\n\t\tdef valid?\n\t\t\t# notice we skip index 0 on purpose, because it has no parent\n\t\t\t(1..(@contents.size - 1)).all? { |e| @contents[e] >= @contents[(e - 1) / 2] }\n\t\tend\n\n\t\tprivate\n\t\t\n\t\t# Left here for reference, but unused.\n\t\t# def swap(i, j)\n\t\t# \t@contents[i], @contents[j] = @contents[j], @contents[i]\n\t\t# end\n\t\t\n\t\tdef bubble_up(index)\n\t\t\tparent_index = (index - 1) / 2 # watch out, integer division!\n\t\t\t\n\t\t\twhile index > 0 && @contents[index] < @contents[parent_index]\n\t\t\t\t# if the node has a smaller value than its parent, swap these nodes\n\t\t\t\t# to uphold the minheap invariant and update the index of the 'current'\n\t\t\t\t# node. If the node is already at index 0, we can also stop because that\n\t\t\t\t# is the root of the heap.\n\t\t\t\t# swap(index, parent_index)\n\t\t\t\t@contents[index], @contents[parent_index] = @contents[parent_index], @contents[index]\n\t\t\t\t\n\t\t\t\tindex = parent_index\n\t\t\t\tparent_index = (index - 1) / 2 # watch out, integer division!\n\t\t\tend\n\t\tend\n\t\t\n\t\tdef bubble_down(index)\n\t\t\tswap_value = 0\n\t\t\tswap_index = nil\n\t\t\t\n\t\t\twhile true\n\t\t\t\tleft_index = (2 * index) + 1\n\t\t\t\tleft_value = @contents[left_index]\n\t\t\t\t\n\t\t\t\tif left_value.nil?\n\t\t\t\t\t# This node has no children so it can't bubble down any further.\n\t\t\t\t\t# We're done here!\n\t\t\t\t\treturn\n\t\t\t\tend\n\t\t\t\t\n\t\t\t\t# Determine which of the child nodes has the smallest value:\n\t\t\t\tright_index = left_index + 1\n\t\t\t\tright_value = @contents[right_index]\n\t\t\t\t\n\t\t\t\tif right_value.nil? or right_value > left_value\n\t\t\t\t\tswap_value = left_value\n\t\t\t\t\tswap_index = left_index\n\t\t\t\telse\n\t\t\t\t\tswap_value = right_value\n\t\t\t\t\tswap_index = right_index\n\t\t\t\tend\n\t\t\t\t\n\t\t\t\tif @contents[index] < swap_value\n\t\t\t\t\t# No need to swap, the minheap invariant is already satisfied:\n\t\t\t\t\treturn\n\t\t\t\telse\n\t\t\t\t\t# 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:\n\t\t\t\t\t# swap(index, swap_index)\n\t\t\t\t\t@contents[index], @contents[swap_index] = @contents[swap_index], @contents[index]\n\t\t\t\t\t\n\t\t\t\t\tindex = swap_index\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\tend\nend\n"
  },
  {
    "path": "lib/timers/timer.rb",
    "content": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2025, by Samuel Williams.\n# Copyright, 2014-2017, by Tony Arcieri.\n# Copyright, 2014, by Utenmiki.\n# Copyright, 2014, by Lin Jen-Shin.\n# Copyright, 2017, by Vít Ondruch.\n# Copyright, 2025, by Patrik Wenger.\n\nmodule Timers\n\t# An individual timer set to fire a given proc at a given time. A timer is\n\t# always connected to a Timer::Group but it would ONLY be in @group.timers\n\t# if it also has a @handle specified. Otherwise it is either PAUSED or has\n\t# been FIRED and is not recurring. You can manually enter this state by\n\t# calling #cancel and resume normal operation by calling #reset.\n\tclass Timer\n\t\tinclude Comparable\n\t\tattr_reader :interval, :offset, :recurring\n\t\t\n\t\tdef initialize(group, interval, recurring = false, offset = nil, &block)\n\t\t\t@group = group\n\t\t\t\n\t\t\t@interval = interval\n\t\t\t@recurring = recurring\n\t\t\t@block = block\n\t\t\t@offset = nil\n\t\t\t@handle = nil\n\t\t\t\n\t\t\t# If a start offset was supplied, use that, otherwise use the current timers offset.\n\t\t\treset(offset || @group.current_offset)\n\t\tend\n\t\t\n\t\tdef paused?\n\t\t\t@group.paused_timers.include? self\n\t\tend\n\t\t\n\t\tdef pause\n\t\t\treturn if paused?\n\t\t\t\n\t\t\t@group.timers.delete self\n\t\t\t@group.paused_timers.add self\n\t\t\t\n\t\t\t@handle.cancel! if @handle\n\t\t\t@handle = nil\n\t\tend\n\t\t\n\t\tdef resume\n\t\t\treturn unless paused?\n\t\t\t\n\t\t\t@group.paused_timers.delete self\n\t\t\t\n\t\t\t# This will add us back to the group:\n\t\t\treset\n\t\tend\n\t\t\n\t\talias continue resume\n\t\t\n\t\t# Extend this timer\n\t\tdef delay(seconds)\n\t\t\t@handle.cancel! if @handle\n\t\t\t\n\t\t\t@offset += seconds\n\t\t\t\n\t\t\t@handle = @group.events.schedule(@offset, self)\n\t\tend\n\t\t\n\t\t# Cancel this timer. Do not call while paused.\n\t\tdef cancel\n\t\t\treturn unless @handle\n\t\t\t\n\t\t\t@handle.cancel! if @handle\n\t\t\t@handle = nil\n\t\t\t\n\t\t\t# This timer is no longer valid:\n\t\t\t@group.timers.delete(self) if @group\n\t\tend\n\t\t\n\t\t# Reset this timer. Do not call while paused.\n\t\t# @param offset [Numeric] the duration to add to the timer.\n\t\tdef reset(offset = @group.current_offset)\n\t\t\t# This logic allows us to minimise the interaction with @group.timers.\n\t\t\t# A timer with a handle is always registered with the group.\n\t\t\tif @handle\n\t\t\t\t@handle.cancel!\n\t\t\telse\n\t\t\t\t@group.timers << self\n\t\t\tend\n\t\t\t\n\t\t\t@offset = Float(offset) + @interval\n\t\t\t\n\t\t\t@handle = @group.events.schedule(@offset, self)\n\t\tend\n\t\t\n\t\t# Fire the block.\n\t\tdef fire(offset = @group.current_offset)\n\t\t\tif recurring == :strict\n\t\t\t\t# ... make the next interval strictly the last offset + the interval:\n\t\t\t\treset(@offset)\n\t\t\telsif recurring\n\t\t\t\treset(offset)\n\t\t\telse\n\t\t\t\t@offset = offset\n\t\t\tend\n\t\t\t\n\t\t\tresult = @block.call(offset, self)\n\t\t\tcancel unless recurring\n\t\t\tresult\n\t\tend\n\t\t\n\t\talias call fire\n\t\t\n\t\t# Number of seconds until next fire / since last fire\n\t\tdef fires_in\n\t\t\t@offset - @group.current_offset if @offset\n\t\tend\n\t\t\n\t\t# Inspect a timer\n\t\tdef inspect\n\t\t\tbuffer = to_s[0..-2]\n\t\t\t\n\t\t\tif @offset\n\t\t\t\tdelta_offset = @offset - @group.current_offset\n\t\t\t\t\n\t\t\t\tif delta_offset > 0\n\t\t\t\t\tbuffer << \" fires in #{delta_offset} seconds\"\n\t\t\t\telse\n\t\t\t\t\tbuffer << \" fired #{delta_offset.abs} seconds ago\"\n\t\t\t\tend\n\t\t\t\t\n\t\t\t\tbuffer << \", recurs every #{interval}\" if recurring\n\t\t\tend\n\t\t\t\n\t\t\tbuffer << \">\"\n\t\t\t\n\t\t\treturn buffer\n\t\tend\n\tend\nend\n"
  },
  {
    "path": "lib/timers/version.rb",
    "content": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2012-2016, by Tony Arcieri.\n# Copyright, 2014-2022, by Samuel Williams.\n# Copyright, 2015, by Donovan Keme.\n\nmodule Timers\n\tVERSION = \"4.4.0\"\nend\n"
  },
  {
    "path": "lib/timers/wait.rb",
    "content": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2025, by Samuel Williams.\n# Copyright, 2014-2016, by Tony Arcieri.\n# Copyright, 2015, by Utenmiki.\n# Copyright, 2015, by Donovan Keme.\n\nrequire_relative \"interval\"\n\nmodule Timers\n\t# An exclusive, monotonic timeout class.\n\tclass Wait\n\t\tdef self.for(duration, &block)\n\t\t\tif duration\n\t\t\t\ttimeout = new(duration)\n\t\t\t\t\n\t\t\t\ttimeout.while_time_remaining(&block)\n\t\t\telse\n\t\t\t\t# If there is no \"duration\" to wait for, we wait forever.\n\t\t\t\tloop do\n\t\t\t\t\tyield(nil)\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\t\t\n\t\tdef initialize(duration)\n\t\t\t@duration = duration\n\t\t\t@remaining = true\n\t\tend\n\t\t\n\t\tattr_reader :duration\n\t\tattr_reader :remaining\n\t\t\n\t\t# Yields while time remains for work to be done:\n\t\tdef while_time_remaining\n\t\t\t@interval = Interval.new\n\t\t\t@interval.start\n\t\t\t\n\t\t\tyield @remaining while time_remaining?\n\t\tensure\n\t\t\t@interval.stop\n\t\t\t@interval = nil\n\t\tend\n\t\t\n\t\tprivate\n\t\t\n\t\tdef time_remaining?\n\t\t\t@remaining = (@duration - @interval.to_f)\n\t\t\t\n\t\t\t@remaining > 0\n\t\tend\n\tend\nend\n"
  },
  {
    "path": "lib/timers.rb",
    "content": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2012-2016, by Tony Arcieri.\n# Copyright, 2012, by Ryan LeCompte.\n# Copyright, 2012, by Nicholas Evans.\n# Copyright, 2012, by Dimitrij Denissenko.\n# Copyright, 2013, by Chuck Remes.\n# Copyright, 2013, by Ron Evans.\n# Copyright, 2013, by Sean Gregory.\n# Copyright, 2013, by Utenmiki.\n# Copyright, 2013, by Jeremy Hinegardner.\n# Copyright, 2014, by Larry Lv.\n# Copyright, 2014, by Bruno Enten.\n# Copyright, 2014-2022, by Samuel Williams.\n# Copyright, 2014, by Mike Bourgeous.\n\nrequire_relative \"timers/version\"\n\nrequire_relative \"timers/group\"\nrequire_relative \"timers/wait\"\n"
  },
  {
    "path": "license.md",
    "content": "# MIT License\n\nCopyright, 2012-2017, by Tony Arcieri.  \nCopyright, 2012, by Ryan LeCompte.  \nCopyright, 2012, by Jesse Cooke.  \nCopyright, 2012, by Nicholas Evans.  \nCopyright, 2012, by Dimitrij Denissenko.  \nCopyright, 2013, by Chuck Remes.  \nCopyright, 2013, by Ron Evans.  \nCopyright, 2013, by Sean Gregory.  \nCopyright, 2013-2015, by Utenmiki.  \nCopyright, 2013, by Jeremy Hinegardner.  \nCopyright, 2014, by Larry Lv.  \nCopyright, 2014, by Bruno Enten.  \nCopyright, 2014-2025, by Samuel Williams.  \nCopyright, 2014, by Mike Bourgeous.  \nCopyright, 2014, by Klaus Trainer.  \nCopyright, 2014, by Lin Jen-Shin.  \nCopyright, 2014, by Lavir the Whiolet.  \nCopyright, 2015-2016, by Donovan Keme.  \nCopyright, 2015, by Tommy Ong Gia Phu.  \nCopyright, 2015, by Will Jessop.  \nCopyright, 2016, by Ryunosuke Sato.  \nCopyright, 2016, by Atul Bhosale.  \nCopyright, 2017, by Vít Ondruch.  \nCopyright, 2017-2020, by Olle Jonsson.  \nCopyright, 2020, by Tim Smith.  \nCopyright, 2021, by Wander Hillen.  \nCopyright, 2022, by Yoshiki Takagi.  \nCopyright, 2023, by Peter Goldstein.  \nCopyright, 2025, by Patrik Wenger.  \n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "readme.md",
    "content": "# Timers\n\nCollections of one-shot and periodic timers, intended for use with event loops such as [async](https://github.com/socketry/async).\n\n[![Development Status](https://github.com/socketry/timers/workflows/Test/badge.svg)](https://github.com/socketry/timers/actions?workflow=Test)\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n``` ruby\ngem 'timers'\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install timers\n\n## Usage\n\nCreate a new timer group with `Timers::Group.new`:\n\n``` ruby\nrequire 'timers'\n\ntimers = Timers::Group.new\n```\n\nSchedule a proc to run after 5 seconds with `Timers::Group#after`:\n\n``` ruby\nfive_second_timer = timers.after(5) { puts \"Take five\" }\n```\n\nThe `five_second_timer` variable is now bound to a Timers::Timer object. To\ncancel a timer, use `Timers::Timer#cancel`\n\nOnce you've scheduled a timer, you can wait until the next timer fires with `Timers::Group#wait`:\n\n``` ruby\n# Waits 5 seconds\ntimers.wait\n\n# The script will now print \"Take five\"\n```\n\nYou can schedule a block to run periodically with `Timers::Group#every`:\n\n``` ruby\nevery_five_seconds = timers.every(5) { puts \"Another 5 seconds\" }\n\nloop { timers.wait }\n```\n\nYou can also schedule a block to run immediately and periodically with `Timers::Group#now_and_every`:\n\n``` ruby\nnow_and_every_five_seconds = timers.now_and_every(5) { puts \"Now and in another 5 seconds\" }\n\nloop { timers.wait }\n```\n\nIf you'd like another method to do the waiting for you, e.g. `Kernel.select`,\nyou can use `Timers::Group#wait_interval` to obtain the amount of time to wait. When\na timeout is encountered, you can fire all pending timers with `Timers::Group#fire`:\n\n``` ruby\nloop do\n  interval = timers.wait_interval\n  ready_readers, ready_writers = select readers, writers, nil, interval\n\n  if ready_readers || ready_writers\n    # Handle IO\n    ...\n  else\n    # Timeout!\n    timers.fire\n  end\nend\n```\n\nYou can also pause and continue individual timers, or all timers:\n\n``` ruby\npaused_timer = timers.every(5) { puts \"I was paused\" }\n\npaused_timer.pause\n10.times { timers.wait } # will not fire paused timer\n\npaused_timer.resume\n10.times { timers.wait } # will fire timer\n\ntimers.pause\n10.times { timers.wait } # will not fire any timers\n\ntimers.resume\n10.times { timers.wait } # will fire all timers\n```\n\n## Contributing\n\nWe welcome contributions to this project.\n\n1.  Fork it.\n2.  Create your feature branch (`git checkout -b my-new-feature`).\n3.  Commit your changes (`git commit -am 'Add some feature'`).\n4.  Push to the branch (`git push origin my-new-feature`).\n5.  Create new Pull Request.\n\n### Developer Certificate of Origin\n\nIn 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.\n\n### Community Guidelines\n\nThis 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.\n"
  },
  {
    "path": "release.cert",
    "content": "-----BEGIN CERTIFICATE-----\nMIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11\nZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK\nCZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz\nMjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd\nMBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj\nbzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB\nigKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2\n9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW\nsGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE\ne5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN\nXibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss\nRZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn\ntUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM\nzp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW\nxm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O\nBBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs\naWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs\naWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE\ncBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl\nxCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/\nc1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp\n8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws\nJkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP\neX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt\nQ2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8\nvoD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "test/timers/events.rb",
    "content": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2025, by Samuel Williams.\n# Copyright, 2014-2016, by Tony Arcieri.\n\nrequire \"timers/events\"\n\ndescribe Timers::Events do\n\tlet(:events) {subject.new}\n\t\n\tit \"should register an event\" do\n\t\tfired = false\n\t\t\n\t\tcallback = proc do |_time|\n\t\t\tfired = true\n\t\tend\n\t\t\n\t\tevents.schedule(0.1, callback)\n\t\t\n\t\texpect(events.size).to be == 1\n\t\t\n\t\tevents.fire(0.15)\n\t\t\n\t\texpect(events.size).to be == 0\n\t\t\n\t\texpect(fired).to be == true\n\tend\n\t\n\tit \"should register events in order\" do\n\t\tfired = []\n\t\t\n\t\ttimes = [0.95, 0.1, 0.3, 0.5, 0.4, 0.2, 0.01, 0.9]\n\t\t\n\t\ttimes.each do |requested_time|\n\t\t\tcallback = proc do |_time|\n\t\t\t\tfired << requested_time\n\t\t\tend\n\t\t\t\n\t\t\tevents.schedule(requested_time, callback)\n\t\tend\n\t\t\n\t\tevents.fire(0.5)\n\t\texpect(fired).to be == times.sort.first(6)\n\t\t\n\t\tevents.fire(1.0)\n\t\texpect(fired).to be == times.sort\n\tend\n\t\n\tit \"should fire events with the time they were fired at\" do\n\t\tfired_at = :not_fired\n\t\t\n\t\tcallback = proc do |time|\n\t\t\t# The time we actually were fired at:\n\t\t\tfired_at = time\n\t\tend\n\t\t\n\t\tevents.schedule(0.5, callback)\n\t\t\n\t\tevents.fire(1.0)\n\t\t\n\t\texpect(fired_at).to be == 1.0\n\tend\n\t\n\tit \"should flush cancelled events\" do\n\t\tcallback = proc{}\n\t\t\n\t\t10.times do\n\t\t\thandle = events.schedule(0.1, callback)\n\t\t\thandle.cancel!\n\t\tend\n\t\t\n\t\texpect(events.size).to be == 1\n\tend\nend\n"
  },
  {
    "path": "test/timers/group/cancel.rb",
    "content": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014, by Lin Jen-Shin.\n# Copyright, 2014-2016, by Tony Arcieri.\n# Copyright, 2014-2025, by Samuel Williams.\n\nrequire \"timers/group\"\n\ndescribe Timers::Group do\n\tlet(:group) {subject.new}\n\t\n\tit \"can cancel a timer\" do\n\t\tfired = false\n\t\t\n\t\ttimer = group.after(0.1) { fired = true }\n\t\ttimer.cancel\n\t\t\n\t\tgroup.wait\n\t\t\n\t\texpect(fired).to be == false\n\tend\n\t\n\tit \"should be able to cancel twice\" do\n\t\tfired = false\n\t\t\n\t\ttimer = group.after(0.1) { fired = true }\n\t\t\n\t\t2.times do\n\t\t\ttimer.cancel\n\t\t\tgroup.wait\n\t\tend\n\t\t\n\t\texpect(fired).to be == false\n\tend\n\t\n\tit \"should be possble to reset after cancel\" do\n\t\tfired = false\n\t\t\n\t\ttimer = group.after(0.1) { fired = true }\n\t\ttimer.cancel\n\t\t\n\t\tgroup.wait\n\t\t\n\t\ttimer.reset\n\t\t\n\t\tgroup.wait\n\t\t\n\t\texpect(fired).to be == true\n\tend\n\t\n\tit \"should cancel and remove one shot timers after they fire\" do\n\t\tx = 0\n\t\t\n\t\tTimers::Wait.for(2) do |_remaining|\n\t\t\ttimer = group.every(0.2) { x += 1 }\n\t\t\tgroup.after(0.1) { timer.cancel }\n\t\t\t\n\t\t\tgroup.wait\n\t\tend\n\t\t\n\t\texpect(group.timers).to be(:empty?)\n\t\texpect(x).to be == 0\n\tend\n\t\n\twith \"#cancel\" do\n\t\tit \"should cancel all timers\" do\n\t\t\ttimers = 3.times.map do\n\t\t\t\tgroup.every(0.1) {}\n\t\t\tend\n\t\t\t\n\t\t\texpect(group.timers).not.to be(:empty?)\n\t\t\t\n\t\t\tgroup.cancel\n\t\t\t\n\t\t\texpect(group.timers).to be(:empty?)\n\t\tend\n\tend\nend\n"
  },
  {
    "path": "test/timers/group/every.rb",
    "content": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2025, by Samuel Williams.\n# Copyright, 2014-2016, by Tony Arcieri.\n# Copyright, 2015, by Tommy Ong Gia Phu.\n# Copyright, 2015, by Donovan Keme.\n\nrequire \"timers/group\"\n\ndescribe Timers::Group do\n\tlet(:group) {subject.new}\n\t\n\tit \"should fire several times\" do\n\t\tresult = []\n\t\t\n\t\tgroup.every(0.7) { result << :a }\n\t\tgroup.every(2.3) { result << :b }\n\t\tgroup.every(1.3) { result << :c }\n\t\tgroup.every(2.4) { result << :d }\n\t\t\n\t\tTimers::Wait.for(2.5) do |remaining|\n\t\t\tgroup.wait if group.wait_interval < remaining\n\t\tend\n\t\t\n\t\texpect(result).to be == [:a, :c, :a, :a, :b, :d]\n\tend\n\t\n\tit \"should fire immediately and then several times later\" do\n\t\tresult = []\n\t\t\n\t\tgroup.every(0.7) { result << :a }\n\t\tgroup.every(2.3) { result << :b }\n\t\tgroup.now_and_every(1.3) { result << :c }\n\t\tgroup.now_and_every(2.4) { result << :d }\n\t\t\n\t\tTimers::Wait.for(2.5) do |remaining|\n\t\t\tgroup.wait if group.wait_interval < remaining\n\t\tend\n\t\t\n\t\texpect(result).to be == [:c, :d, :a, :c, :a, :a, :b, :d]\n\tend\nend\n"
  },
  {
    "path": "test/timers/group/pause.rb",
    "content": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2022-2025, by Samuel Williams.\n\nrequire \"timers/group\"\n\ndescribe Timers::Group do\n\tlet(:group) {subject.new}\n\tlet(:interval) {0.01}\n\t\n\tdef before\n\t\t@fired = false\n\t\t@timer = group.after(interval) {@fired = true}\n\t\t\n\t\t@fired2 = false\n\t\t@timer2 = group.after(interval) {@fired2 = true}\n\t\t\n\t\tsuper\n\tend\n\t\n\tit \"does not fire when paused\" do\n\t\t@timer.pause\n\t\tgroup.wait\n\t\texpect(@fired).to be == false\n\tend\n\t\n\tit \"fires when continued after pause\" do\n\t\t@timer.pause\n\t\tgroup.wait\n\t\t@timer.resume\n\t\t\n\t\tsleep(interval)\n\t\tgroup.wait\n\t\t\n\t\texpect(@fired).to be == true\n\tend\n\t\n\tit \"can pause all timers at once\" do\n\t\tgroup.pause\n\t\tgroup.wait\n\t\t\n\t\texpect(@fired).to be == false\n\t\texpect(@fired2).to be == false\n\tend\n\t\n\tit \"can continue all timers at once\" do\n\t\tgroup.pause\n\t\tgroup.wait\n\t\tgroup.resume\n\t\t\n\t\tsleep(interval + TIMER_QUANTUM)\n\t\tgroup.wait\n\t\t\n\t\texpect(@fired).to be == true\n\t\texpect(@fired2).to be == true\n\tend\n\t\n\tit \"can fire the timer directly\" do\n\t\t@timer.pause\n\t\t\n\t\tgroup.wait\n\t\texpect(@fired).not.to be == true\n\t\t\n\t\t@timer.resume\n\t\texpect(@fired).not.to be == true\n\n\t\t@timer.fire\n\t\texpect(@fired).to be == true\n\tend\nend\n"
  },
  {
    "path": "test/timers/group.rb",
    "content": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2012-2017, by Tony Arcieri.\n# Copyright, 2012, by Jesse Cooke.\n# Copyright, 2012, by Dimitrij Denissenko.\n# Copyright, 2013, by Chuck Remes.\n# Copyright, 2013, by Ron Evans.\n# Copyright, 2013, by Sean Gregory.\n# Copyright, 2013-2014, by Utenmiki.\n# Copyright, 2013, by Jeremy Hinegardner.\n# Copyright, 2014, by Bruno Enten.\n# Copyright, 2014-2025, by Samuel Williams.\n# Copyright, 2017, by Vít Ondruch.\n\nrequire \"timers/group\"\nrequire \"timer_quantum\"\n\ndescribe Timers::Group do\n\tlet(:group) {subject.new}\n\t\n\twith \"#wait\" do\n\t\tit \"calls the wait block with nil\" do\n\t\t\tcalled = false\n\t\t\t\n\t\t\tgroup.wait do |interval|\n\t\t\t\texpect(interval).to be_nil\n\t\t\t\tcalled = true\n\t\t\tend\n\t\t\t\n\t\t\texpect(called).to be == true\n\t\tend\n\t\t\n\t\tit \"calls the wait block with an interval\" do\n\t\t\tcalled = false\n\t\t\tfired = false\n\t\t\t\n\t\t\tgroup.after(0.1) { fired = true }\n\t\t\t\n\t\t\tgroup.wait do |interval|\n\t\t\t\texpect(interval).to be_within(TIMER_QUANTUM).of(0.1)\n\t\t\t\tcalled = true\n\t\t\t\tsleep 0.2\n\t\t\tend\n\t\t\t\n\t\t\texpect(called).to be == true\n\t\t\texpect(fired).to be == true\n\t\tend\n\t\t\n\t\tit \"repeatedly calls the wait block if it sleeps less than the interval\" do\n\t\t\tcalled = 0\n\t\t\tfired = false\n\t\t\t\n\t\t\tgroup.after(0.1) { fired = true }\n\t\t\t\n\t\t\tgroup.wait do |interval|\n\t\t\t\tcalled += 1\n\t\t\t\tsleep(0.01)\n\t\t\tend\n\t\t\t\n\t\t\texpect(called).to be > 1\n\t\t\texpect(fired).to be == true\n\t\tend\n\tend\n\t\n\tit \"sleeps until the next timer\" do\n\t\tinterval = 0.1\n\t\t\n\t\tfired = false\n\t\tgroup.after(interval) {fired = true}\n\t\tgroup.wait\n\t\t\n\t\texpect(fired).to be == true\n\tend\n\t\n\tit \"fires instantly when next timer is in the past\" do\n\t\tfired = false\n\t\tgroup.after(TIMER_QUANTUM) { fired = true }\n\t\tsleep(TIMER_QUANTUM * 2)\n\t\tgroup.wait\n\t\t\n\t\texpect(fired).to be == true\n\tend\n\t\n\tit \"calculates the interval until the next timer should fire\" do\n\t\tinterval = 0.1\n\t\t\n\t\tgroup.after(interval)\n\t\texpect(group.wait_interval).to be_within(TIMER_QUANTUM).of interval\n\t\t\n\t\tsleep(interval)\n\t\texpect(group.wait_interval).to be <= 0\n\tend\n\t\n\tit \"fires timers in the correct order\" do\n\t\tresult = []\n\t\t\n\t\tgroup.after(TIMER_QUANTUM * 2) { result << :two }\n\t\tgroup.after(TIMER_QUANTUM * 3) { result << :three }\n\t\tgroup.after(TIMER_QUANTUM * 1) { result << :one }\n\t\t\n\t\tsleep(TIMER_QUANTUM * 4)\n\t\tgroup.fire\n\t\t\n\t\texpect(result).to be == [:one, :two, :three]\n\tend\n\t\n\tit \"raises TypeError if given an invalid time\" do\n\t\texpect do\n\t\t\tgroup.after(nil) { nil }\n\t\tend.to raise_exception(TypeError)\n\tend\n\t\n\twith \"#now_and_after\" do\n\t\tit \"fires the timer immediately\" do\n\t\t\tresult = []\n\t\t\t\n\t\t\tgroup.now_and_after(TIMER_QUANTUM * 2) { result << :foo }\n\t\t\t\n\t\t\texpect(result).to be == [:foo]\n\t\tend\n\t\t\n\t\tit \"fires the timer at the correct time\" do\n\t\t\tresult = []\n\t\t\t\n\t\t\tgroup.now_and_after(TIMER_QUANTUM * 2) { result << :foo }\n\t\t\t\n\t\t\tgroup.wait\n\t\t\t\n\t\t\texpect(result).to be == [:foo, :foo]\n\t\tend\n\tend\n\t\n\twith \"recurring timers\" do\n\t\tit \"continues to fire the timers at each interval\" do\n\t\t\tresult = []\n\t\t\t\n\t\t\tgroup.every(TIMER_QUANTUM * 2) { result << :foo }\n\t\t\t\n\t\t\tsleep TIMER_QUANTUM * 3\n\t\t\tgroup.fire\n\t\t\texpect(result).to be == [:foo]\n\t\t\t\n\t\t\tsleep TIMER_QUANTUM * 5\n\t\t\tgroup.fire\n\t\t\texpect(result).to be == [:foo, :foo]\n\t\tend\n\tend\n\t\n\tit \"calculates the proper interval to wait until firing\" do\n\t\tinterval_ms = 25\n\t\t\n\t\tgroup.after(interval_ms / 1000.0)\n\t\t\n\t\texpect(group.wait_interval).to be_within(TIMER_QUANTUM).of(interval_ms / 1000.0)\n\tend\n\t\n\twith \"delay timer\" do\n\t\tit \"adds appropriate amount of time to timer\" do\n\t\t\ttimer = group.after(10)\n\t\t\ttimer.delay(5)\n\t\t\texpect(timer.offset - group.current_offset).to be_within(TIMER_QUANTUM).of(15)\n\t\tend\n\tend\n\t\n\twith \"delay timer collection\" do\n\t\tit \"delay on set adds appropriate amount of time to all timers\" do\n\t\t\ttimer = group.after(10)\n\t\t\ttimer2 = group.after(20)\n\t\t\tgroup.delay(5)\n\t\t\texpect(timer.offset - group.current_offset).to be_within(TIMER_QUANTUM).of(15)\n\t\t\texpect(timer2.offset - group.current_offset).to be_within(TIMER_QUANTUM).of(25)\n\t\tend\n\tend\n\t\n\twith \"on delaying a timer\" do\n\t\tit \"fires timers in the correct order\" do\n\t\t\tresult = []\n\t\t\t\n\t\t\tgroup.after(TIMER_QUANTUM * 2) { result << :two }\n\t\t\tgroup.after(TIMER_QUANTUM * 3) { result << :three }\n\t\t\tfirst = group.after(TIMER_QUANTUM * 1) { result << :one }\n\t\t\tfirst.delay(TIMER_QUANTUM * 3)\n\t\t\t\n\t\t\tsleep TIMER_QUANTUM * 5\n\t\t\tgroup.fire\n\t\t\t\n\t\t\texpect(result).to be == [:two, :three, :one]\n\t\tend\n\tend\n\t\n\twith \"#inspect\" do\n\t\tit \"before firing\" do\n\t\t\tfired = false\n\t\t\ttimer = group.after(TIMER_QUANTUM * 5) { fired = true }\n\t\t\ttimer.pause\n\t\t\texpect(fired).not.to be == true\n\t\t\texpect(timer.inspect).to be =~ /\\A#<Timers::Timer:0x[\\da-f]+ fires in [-\\.\\de]+ seconds>\\Z/\n\t\tend\n\t\t\n\t\tit \"after firing\" do\n\t\t\tfired = false\n\t\t\ttimer = group.after(TIMER_QUANTUM) { fired = true }\n\t\t\t\n\t\t\tgroup.wait\n\t\t\t\n\t\t\texpect(fired).to be == true\n\t\t\texpect(timer.inspect).to be =~/\\A#<Timers::Timer:0x[\\da-f]+ fired [-\\.\\de]+ seconds ago>\\Z/\n\t\tend\n\t\t\n\t\tit \"recurring firing\" do\n\t\t\tresult = []\n\t\t\ttimer = group.every(TIMER_QUANTUM) { result << :foo }\n\t\t\t\n\t\t\tgroup.wait\n\t\t\texpect(result).to be(:any?)\n\t\t\tregex = /\\A#<Timers::Timer:0x[\\da-f]+ fires in [-\\.\\de]+ seconds, recurs every #{TIMER_QUANTUM}>\\Z/\n\t\t\texpect(timer.inspect).to be =~ regex\n\t\tend\n\tend\n\t\n\twith \"#fires_in\" do\n\t\tlet(:interval) {0.01}\n\t\t\n\t\twith \"recurring timer\" do\n\t\t\tlet(:timer) {group.every(interval){true}}\n\n\t\t\tit \"calculates the interval until the next fire if it's recurring\" do\n\t\t\t\texpect(timer.fires_in).to be_within(TIMER_QUANTUM).of(interval)\n\t\t\tend\n\t\tend\n\t\t\n\t\twith \"non-recurring timer\" do\n\t\t\tlet(:timer) {group.after(interval){true}}\n\t\t\t\n\t\t\tit \"calculates the interval until the next fire if it hasn't already fired\" do\n\t\t\t\texpect(timer.fires_in).to be_within(TIMER_QUANTUM).of(interval)\n\t\t\tend\n\t\t\t\n\t\t\tit \"calculates the interval since last fire if already fired\" do\n\t\t\t\t# Create the timer:\n\t\t\t\ttimer\n\t\t\t\t\n\t\t\t\tgroup.wait\n\t\t\t\t\n\t\t\t\tsleep(TIMER_QUANTUM)\n\t\t\t\t\n\t\t\t\texpect(timer.fires_in).to be < 0.0\n\t\t\tend\n\t\tend\n\tend\nend\n"
  },
  {
    "path": "test/timers/performance.rb",
    "content": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2025, by Samuel Williams.\n# Copyright, 2014-2016, by Tony Arcieri.\n# Copyright, 2015, by Donovan Keme.\n# Copyright, 2021, by Wander Hillen.\n\n# Event based timers:\n\n# Serviced 31812 events in 2.39075272 seconds, 13306.320832794887 e/s.\n# Thread ID: 7336700\n# Fiber ID: 30106340\n# Total: 2.384043\n# Sort by: self_time\n\n# %self      total      self      wait     child     calls  name\n# 13.48      0.510     0.321     0.000     0.189    369133  Timers::Events::Handle#<=>\n#  8.12      0.194     0.194     0.000     0.000    427278  Timers::Events::Handle#to_f\n#  4.55      0.109     0.109     0.000     0.000    427278  Float#<=>\n#  4.40      1.857     0.105     0.000     1.752    466376 *Timers::Events#bsearch\n#  4.30      0.103     0.103     0.000     0.000    402945  Float#to_f\n#  2.65      0.063     0.063     0.000     0.000     33812  Array#insert\n#  2.64      1.850     0.063     0.000     1.787     33812  Timers::Events#schedule\n#  2.40      1.930     0.057     0.000     1.873     33812  Timers::Timer#reset\n#  1.89      1.894     0.045     0.000     1.849     31812  Timers::Timer#fire\n#  1.69      1.966     0.040     0.000     1.926     31812  Timers::Events::Handle#fire\n#  1.35      0.040     0.032     0.000     0.008     33812  Timers::Events::Handle#initialize\n#  1.29      0.044     0.031     0.000     0.013     44451  Timers::Group#current_offset\n\n# SortedSet based timers:\n\n# Serviced 32516 events in 66.753277275 seconds, 487.1072288781219 e/s.\n# Thread ID: 15995640\n# Fiber ID: 38731780\n# Total: 66.716394\n# Sort by: self_time\n\n# %self      total      self      wait     child     calls  name\n# 54.73     49.718    36.513     0.000    13.205  57084873  Timers::Timer#<=>\n# 23.74     65.559    15.841     0.000    49.718     32534  Array#sort!\n# 19.79     13.205    13.205     0.000     0.000  57084873  Float#<=>\n\n# Max out events performance (on my computer):\n# Serviced 1142649 events in 11.194903921 seconds, 102068.70405115146 e/s.\n\nrequire \"timers/group\"\n\ndescribe Timers::Group do\n\tlet(:group) {subject.new}\n\t\n\twith \"profiler\" do\n\t\tif defined? RubyProf\n\t\t\tdef before\n\t\t\t\t# Running RubyProf makes the code slightly slower.\n\t\t\t\tRubyProf.start\n\t\t\t\tputs \"*** Running with RubyProf reduces performance ***\"\n\t\t\t\t\n\t\t\t\tsuper\n\t\t\tend\n\t\t\t\n\t\t\tdef after\n\t\t\t\tsuper\n\t\t\t\t\n\t\t\t\tif RubyProf.running?\n\t\t\t\t\t# file = arg.metadata[:description].gsub(/\\s+/, '-')\n\t\t\t\t\t\n\t\t\t\t\tresult = RubyProf.stop\n\t\t\t\t\t\n\t\t\t\t\tprinter = RubyProf::FlatPrinter.new(result)\n\t\t\t\t\tprinter.print($stderr, min_percent: 1.0)\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\t\t\n\t\tit \"runs efficiently\" do\n\t\t\tresult = []\n\t\t\trange = (1..500)\n\t\t\tduration = 2.0\n\t\t\t\n\t\t\ttotal = 0\n\t\t\trange.each do |index|\n\t\t\t\toffset = index.to_f / range.max\n\t\t\t\ttotal += (duration / offset).floor\n\t\t\t\t\n\t\t\t\tgroup.every(index.to_f / range.max, :strict) { result << index }\n\t\t\tend\n\t\t\t\n\t\t\tgroup.wait while result.size < total\n\t\t\t\n\t\t\trate = result.size.to_f / group.current_offset\n\t\t\tinform \"Serviced #{result.size} events in #{group.current_offset} seconds, #{rate} e/s.\"\n\t\t\t\n\t\t\texpect(group.current_offset).to be_within(20).percent_of(duration)\n\t\tend\n\tend\n\t\n\tit \"runs efficiently at high volume\" do\n\t\tresults = []\n\t\trange = (1..300)\n\t\tgroups = (1..20)\n\t\tduration = 2.0\n\t\t\n\t\ttimers = []\n\t\t@mutex = Mutex.new\n\t\tstart = Time.now\n\t\t\n\t\tgroups.each do\n\t\t\ttimers << Thread.new do\n\t\t\t\tresult = []\n\t\t\t\ttimer = Timers::Group.new\n\t\t\t\ttotal = 0\n\t\t\t\t\n\t\t\t\trange.each do |index|\n\t\t\t\t\toffset = index.to_f / range.max\n\t\t\t\t\ttotal += (duration / offset).floor\n\t\t\t\t\ttimer.every(index.to_f / range.max, :strict) { result << index }\n\t\t\t\tend\n\t\t\t\t\n\t\t\t\ttimer.wait while result.size < total\n\t\t\t\t@mutex.synchronize { results += result }\n\t\t\tend\n\t\tend\n\t\t\n\t\ttimers.each { |t| t.join }\n\t\tfinish = Time.now\n\t\t\n\t\truntime = finish - start\n\t\trate = results.size.to_f / runtime\n\t\t\n\t\tinform \"Serviced #{results.size} events in #{runtime} seconds, #{rate} e/s; across #{groups.max} timers.\"\n\t\t\n\t\texpect(runtime).to be_within(20).percent_of(duration)\n\tend\n\t\n\tit \"copes with very large amounts of timers\" do\n\t\t# This spec tries to emulate (as best as possible) the timer characteristics of the\n\t\t# following scenario:\n\t\t# - a fairly busy Falcon server serving a constant stream of request that spend most of their time\n\t\t#   in a long database call. Both the web request and the db call have a timeout attached\n\t\t# - there will already exist a lot of timers in the queue and more are added all the time\n\t\t# - the server is assumed to be busy so there are \"always\" new requests waiting to be accept()-ed\n\t\t#   and thus the server spends relatively little time actually sleeping and most of its time in\n\t\t#   either the reactor or an active fiber.\n\t\t# - On each loop of the reactor it will run any fibers in the ready queue, accept any waiting\n\t\t#   requests on the server socket and then call wait_interval to see if there are any expired\n\t\t#   timeouts that need to be handled.\n\t\t\n\t\t# Result for PriorityHeap based timer queue: Inserted 20k timers in 0.055050924 seconds\n\t\t# Result for Array based timer queue: \t\t\t Inserted 20k timers in 0.141001845 seconds\n\t\t\n\t\tresults = []\n\t\t\n\t\t# Prefill the timer queue with a lot of timers in the semidistant future\n\t\t20000.times do\n\t\t\tgroup.after(10) { results << \"yay!\" }\n\t\tend\n\t\t\n\t\t# add one timer which is done immediately, to get the pending array into the queue\n\t\tgroup.after(-1) { results << \"I am first!\" }\n\t\tgroup.wait\n\t\texpect(results.size).to be == 1\n\t\texpect(results.first).to be == \"I am first!\"\n\t\t\n\t\t# 20k extra requests come in and get added into the queue\n\t\tstart = Time.now\n\t\t\n\t\t20000.times do\n\t\t\t# add new timer to the queue (later than all the others so far)\n\t\t\tgroup.after(15) { result << \"yay again!\" }\n\t\t\t# wait_interval in the reactor loop\n\t\t\tgroup.wait_interval()\n\t\tend\n\t\t\n\t\texpect(group.events.size).to be == 40_000\n\t\tinform \"Inserted 20k timers in #{Time.now - start} seconds\"\n\tend\nend\n"
  },
  {
    "path": "test/timers/priority_heap.rb",
    "content": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2021, by Wander Hillen.\n# Copyright, 2021-2025, by Samuel Williams.\n\nrequire \"timers/priority_heap\"\n\ndescribe Timers::PriorityHeap do\n\tlet(:priority_heap) {subject.new}\n\t\n\twith \"empty heap\" do \n\t\tit \"should return nil when the first element is requested\" do\n\t\t\texpect(priority_heap.peek).to be_nil\n\t\tend\n\t\t\n\t\tit \"should return nil when the first element is extracted\" do\n\t\t\texpect(priority_heap.pop).to be_nil\n\t\tend\n\t\t\n\t\tit \"should report its size as zero\" do\n\t\t\texpect(priority_heap.size).to be(:zero?)\n\t\tend\n\tend\n\t\n\tit \"returns the same element after inserting a single element\" do\n\t\tpriority_heap.push(1)\n\t\texpect(priority_heap.size).to be == 1\n\t\texpect(priority_heap.pop).to be == 1\n\t\texpect(priority_heap.size).to be(:zero?)\n\tend\n\t\n\tit \"should return inserted elements in ascending order no matter the insertion order\" do\n\t\t(1..10).to_a.shuffle.each do |e|\n\t\t\tpriority_heap.push(e)\n\t\tend\n\t\t\n\t\texpect(priority_heap.size).to be == 10\n\t\texpect(priority_heap.peek).to be == 1\n\t\t\n\t\tresult = []\n\t\t10.times do\n\t\t\tresult << priority_heap.pop\n\t\tend\n\t\t\n\t\texpect(result.size).to be == 10\n\t\texpect(priority_heap.size).to be(:zero?)\n\t\texpect(result.sort).to be == result\n\tend\n\n\twith \"maintaining the heap invariant\" do\n\t\tit \"for empty heaps\" do\n\t\t\texpect(priority_heap).to be(:valid?)\n\t\tend\n\n\t\tit \"for heap of size 1\" do\n\t\t\tpriority_heap.push(123)\n\t\t\texpect(priority_heap).to be(:valid?)\n\t\tend\n\t\t# Exhaustive testing of all permutations of [1..6]\n\t\tit \"for all permutations of size 6\" do\n\t\t\t[1,2,3,4,5,6].permutation do |arr|\n\t\t\t\tpriority_heap.clear!\n\t\t\t\tarr.each { |e| priority_heap.push(e) }\n\t\t\t\texpect(priority_heap).to be(:valid?)\n\t\t\tend\n\t\tend\n\n\t\t# A few examples with more elements (but not ALL permutations)\n\t\tit \"for larger amounts of values\" do\n\t\t\t5.times do\n\t\t\t\tpriority_heap.clear!\n\t\t\t\t(1..1000).to_a.shuffle.each { |e| priority_heap.push(e) }\n\t\t\t\texpect(priority_heap).to be(:valid?)\n\t\t\tend\n\t\tend\n\n\t\t# What if we insert several of the same item along with others?\n\t\tit \"with several elements of the same value\" do\n\t\t\ttest_values = (1..10).to_a + [4] * 5\n\t\t\ttest_values.each { |e| priority_heap.push(e) }\n\t\t\texpect(priority_heap).to be(:valid?)\n\t\tend\n\tend\nend\n"
  },
  {
    "path": "test/timers/strict.rb",
    "content": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2025, by Samuel Williams.\n# Copyright, 2014-2016, by Tony Arcieri.\n\nrequire \"timers/group\"\nrequire \"timer_quantum\"\n\ndescribe Timers::Group do\n\tlet(:group) {subject.new}\n\t\n\tit \"should not diverge too much\" do\n\t\tfired = :not_fired_yet\n\t\tcount = 0\n\t\tquantum = 0.01\n\t\t\n\t\tstart_offset = group.current_offset\n\t\tTimers::Timer.new(group, quantum, :strict, start_offset) do |offset|\n\t\t\tfired = offset\n\t\t\tcount += 1\n\t\tend\n\t\t\n\t\titerations = 100\n\t\tgroup.wait while count < iterations\n\t\t\n\t\t# In my testing on the JVM, without the :strict recurring, I noticed 60ms of error here.\n\t\texpect(fired - start_offset).to be_within(quantum + TIMER_QUANTUM).of(iterations * quantum)\n\tend\n\t\n\tit \"should only fire 0-interval timer once per iteration\" do\n\t\tcount = 0\n\t\t\n\t\tstart_offset = group.current_offset\n\t\tTimers::Timer.new(group, 0, :strict, start_offset) do |offset, timer|\n\t\t\tcount += 1\n\t\tend\n\t\t\n\t\tgroup.wait\n\t\t\n\t\texpect(count).to be == 1\n\tend\nend\n"
  },
  {
    "path": "test/timers/timer.rb",
    "content": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2025, by Patrik Wenger.\n# Copyright, 2025, by Samuel Williams.\n\nrequire \"timers/timer\"\n\ndescribe Timers::Timer do\n\tlet(:group) {Timers::Group.new}\n\t\n\tit \"should return the block value when fired\" do\n\t\ttimer  = group.after(10) {:foo}\n\t\tresult = timer.fire\n\n\t\texpect(result).to be == :foo\n\tend\nend\n"
  },
  {
    "path": "test/timers/wait.rb",
    "content": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2014-2025, by Samuel Williams.\n# Copyright, 2014-2016, by Tony Arcieri.\n\nrequire \"timers/wait\"\nrequire \"timer_quantum\"\n\ndescribe Timers::Wait do\n\tlet(:interval) {0.1}\n\tlet(:repeats) {10}\n\t\n\tit \"repeats until timeout expired\" do\n\t\ttimeout = Timers::Wait.new(interval*repeats)\n\t\tcount = 0\n\t\tprevious_remaining = nil\n\t\t\n\t\ttimeout.while_time_remaining do |remaining|\n\t\t\tif previous_remaining\n\t\t\t\texpect(remaining).to be_within(TIMER_QUANTUM).of(previous_remaining - interval)\n\t\t\tend\n\t\t\t\n\t\t\tprevious_remaining = remaining\n\t\t\t\n\t\t\tcount += 1\n\t\t\tsleep(interval)\n\t\tend\n\t\t\n\t\texpect(count).to be == repeats\n\tend\n\t\n\tit \"yields results as soon as possible\" do\n\t\ttimeout = Timers::Wait.new(5)\n\t\t\n\t\tresult = timeout.while_time_remaining do |_remaining|\n\t\t\tbreak :done\n\t\tend\n\t\t\n\t\texpect(result).to be == :done\n\tend\n\t\n\twith \"#for\" do\n\t\twith \"no duration\" do\n\t\t\tit \"waits forever\" do\n\t\t\t\tcount = 0\n\t\t\t\tTimers::Wait.for(nil) do\n\t\t\t\t\tcount += 1\n\t\t\t\t\tbreak if count > 10\n\t\t\t\tend\n\t\t\t\t\n\t\t\t\texpect(count).to be > 10\n\t\t\tend\n\t\tend\n\tend\nend\n"
  },
  {
    "path": "timers.gemspec",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"lib/timers/version\"\n\nGem::Specification.new do |spec|\n\tspec.name = \"timers\"\n\tspec.version = Timers::VERSION\n\t\n\tspec.summary = \"Pure Ruby one-shot and periodic timers.\"\n\tspec.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\"]\n\tspec.license = \"MIT\"\n\t\n\tspec.cert_chain  = [\"release.cert\"]\n\tspec.signing_key = File.expand_path(\"~/.gem/release.pem\")\n\t\n\tspec.homepage = \"https://github.com/socketry/timers\"\n\t\n\tspec.metadata = {\n\t\t\"source_code_uri\" => \"https://github.com/socketry/timers.git\",\n\t}\n\t\n\tspec.files = Dir.glob([\"{lib}/**/*\", \"*.md\"], File::FNM_DOTMATCH, base: __dir__)\n\t\n\tspec.required_ruby_version = \">= 3.1\"\nend\n"
  }
]