timers 4.3.0 → 4.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ccf7aa14dc18086e3a47fb44eb3a12569ba6752e6f7d22aecf7486684a594331
4
- data.tar.gz: 0c37cdab96fc3ae8a1fbd0deb07b69800260b32118c7d1b37d2a4114258c32e8
3
+ metadata.gz: e7c61c9db77955d85c69ca83499add3629c71628af3a825d78978b2383182b4a
4
+ data.tar.gz: 27f570753d83d4b4a98ee6ede089908d571347df8ba411edb56fe4ae965e703c
5
5
  SHA512:
6
- metadata.gz: dcb1826464d594d78d0a67bf18f89bb3137ffdbdcb36306176bbaa961aab387b93442c4827f5a8e331b90de056dbc9f9f63dbdeb22671dcdd35203741f415967
7
- data.tar.gz: 43dcaccab4fab882ca498ec196f08d47beb2636044b62103640406e5a0411d1eb6d837363eae50266c11642a59880179eca6a9f4053aefab1b4daf11a28319bd
6
+ metadata.gz: a7bac196cc0e3e1a784f7ba961c4a09539c875f80ac366da0b0d091d9f6ad1a505fb0a86a0c9d61d44a3495342029d003749b3ac072904b37f667d8e6e90b356
7
+ data.tar.gz: 4fa05113f72aef3abe63804a097e7fd9322c5b84a2a9ad14d590906e3ca59754857378a085388c52f9eb7ef1beb934b6b75789c3c087eee0b900f3446ace7207
checksums.yaml.gz.sig ADDED
Binary file
data/lib/timers/events.rb CHANGED
@@ -1,128 +1,111 @@
1
1
  # frozen_string_literal: true
2
- #
3
- # This file is part of the "timers" project and released under the MIT license.
4
- #
5
- # Copyright, 2018, by Samuel Williams. All rights reserved.
6
- #
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2014-2022, by Samuel Williams.
5
+ # Copyright, 2014-2016, by Tony Arcieri.
6
+ # Copyright, 2014, by Lavir the Whiolet.
7
+ # Copyright, 2015, by Utenmiki.
8
+ # Copyright, 2015, by Donovan Keme.
9
+ # Copyright, 2021, by Wander Hillen.
7
10
 
8
11
  require_relative "timer"
12
+ require_relative "priority_heap"
9
13
 
10
14
  module Timers
11
- # Maintains an ordered list of events, which can be cancelled.
15
+ # Maintains a PriorityHeap of events ordered on time, which can be cancelled.
12
16
  class Events
13
17
  # Represents a cancellable handle for a specific timer event.
14
18
  class Handle
19
+ include Comparable
20
+
15
21
  def initialize(time, callback)
16
22
  @time = time
17
23
  @callback = callback
18
24
  end
19
-
25
+
20
26
  # The absolute time that the handle should be fired at.
21
27
  attr_reader :time
22
-
28
+
23
29
  # Cancel this timer, O(1).
24
30
  def cancel!
25
31
  # The simplest way to keep track of cancelled status is to nullify the
26
32
  # callback. This should also be optimal for garbage collection.
27
33
  @callback = nil
28
34
  end
29
-
35
+
30
36
  # Has this timer been cancelled? Cancelled timer's don't fire.
31
37
  def cancelled?
32
38
  @callback.nil?
33
39
  end
34
-
35
- def > other
36
- @time > other.to_f
37
- end
38
-
39
- def >= other
40
- @time >= other.to_f
41
- end
42
-
43
- def to_f
44
- @time
40
+
41
+ def <=> other
42
+ @time <=> other.time
45
43
  end
46
-
44
+
47
45
  # Fire the callback if not cancelled with the given time parameter.
48
46
  def fire(time)
49
47
  @callback.call(time) if @callback
50
48
  end
51
49
  end
52
-
50
+
53
51
  def initialize
54
52
  # A sequence of handles, maintained in sorted order, future to present.
55
53
  # @sequence.last is the next event to be fired.
56
- @sequence = []
54
+ @sequence = PriorityHeap.new
57
55
  @queue = []
58
56
  end
59
-
57
+
60
58
  # Add an event at the given time.
61
59
  def schedule(time, callback)
60
+ flush!
61
+
62
62
  handle = Handle.new(time.to_f, callback)
63
63
 
64
64
  @queue << handle
65
65
 
66
66
  return handle
67
67
  end
68
-
68
+
69
69
  # Returns the first non-cancelled handle.
70
70
  def first
71
71
  merge!
72
72
 
73
- while (handle = @sequence.last)
73
+ while (handle = @sequence.peek)
74
74
  return handle unless handle.cancelled?
75
75
  @sequence.pop
76
76
  end
77
77
  end
78
-
78
+
79
79
  # Returns the number of pending (possibly cancelled) events.
80
80
  def size
81
81
  @sequence.size + @queue.size
82
82
  end
83
-
83
+
84
84
  # Fire all handles for which Handle#time is less than the given time.
85
85
  def fire(time)
86
86
  merge!
87
87
 
88
- while handle = @sequence.last and handle.time <= time
88
+ while handle = @sequence.peek and handle.time <= time
89
89
  @sequence.pop
90
90
  handle.fire(time)
91
91
  end
92
92
  end
93
-
93
+
94
94
  private
95
-
95
+
96
+ # Move all non-cancelled timers from the pending queue to the priority heap
96
97
  def merge!
97
98
  while handle = @queue.pop
98
99
  next if handle.cancelled?
99
100
 
100
- index = bisect_right(@sequence, handle)
101
-
102
- if current_handle = @sequence[index] and current_handle.cancelled?
103
- # puts "Replacing handle at index: #{index} due to cancellation in array containing #{@sequence.size} item(s)."
104
- @sequence[index] = handle
105
- else
106
- # puts "Inserting handle at index: #{index} in array containing #{@sequence.size} item(s)."
107
- @sequence.insert(index, handle)
108
- end
101
+ @sequence.push(handle)
109
102
  end
110
103
  end
111
-
112
- # Return the right-most index where to insert item e, in a list a, assuming
113
- # a is sorted in descending order.
114
- def bisect_right(a, e, l = 0, u = a.length)
115
- while l < u
116
- m = l + (u - l).div(2)
117
-
118
- if a[m] >= e
119
- l = m + 1
120
- else
121
- u = m
122
- end
104
+
105
+ def flush!
106
+ while @queue.last&.cancelled?
107
+ @queue.pop
123
108
  end
124
-
125
- l
126
109
  end
127
110
  end
128
111
  end
data/lib/timers/group.rb CHANGED
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
- #
3
- # This file is part of the "timers" project and released under the MIT license.
4
- #
5
- # Copyright, 2018, by Samuel Williams. All rights reserved.
6
- #
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2014-2022, by Samuel Williams.
5
+ # Copyright, 2014-2016, by Tony Arcieri.
6
+ # Copyright, 2015, by Donovan Keme.
7
+ # Copyright, 2015, by Tommy Ong Gia Phu.
7
8
 
8
9
  require "set"
9
10
  require "forwardable"
@@ -16,62 +17,62 @@ module Timers
16
17
  # A collection of timers which may fire at different times
17
18
  class Group
18
19
  include Enumerable
19
-
20
+
20
21
  extend Forwardable
21
22
  def_delegators :@timers, :each, :empty?
22
-
23
+
23
24
  def initialize
24
25
  @events = Events.new
25
-
26
+
26
27
  @timers = Set.new
27
28
  @paused_timers = Set.new
28
-
29
+
29
30
  @interval = Interval.new
30
31
  @interval.start
31
32
  end
32
-
33
+
33
34
  # Scheduled events:
34
35
  attr_reader :events
35
-
36
+
36
37
  # Active timers:
37
38
  attr_reader :timers
38
-
39
+
39
40
  # Paused timers:
40
41
  attr_reader :paused_timers
41
-
42
+
42
43
  # Call the given block after the given interval. The first argument will be
43
44
  # the time at which the group was asked to fire timers for.
44
45
  def after(interval, &block)
45
46
  Timer.new(self, interval, false, &block)
46
47
  end
47
-
48
+
48
49
  # Call the given block immediately, and then after the given interval. The first
49
50
  # argument will be the time at which the group was asked to fire timers for.
50
51
  def now_and_after(interval, &block)
51
52
  yield
52
53
  after(interval, &block)
53
54
  end
54
-
55
+
55
56
  # Call the given block periodically at the given interval. The first
56
57
  # argument will be the time at which the group was asked to fire timers for.
57
58
  def every(interval, recur = true, &block)
58
59
  Timer.new(self, interval, recur, &block)
59
60
  end
60
-
61
+
61
62
  # Call the given block immediately, and then periodically at the given interval. The first
62
63
  # argument will be the time at which the group was asked to fire timers for.
63
64
  def now_and_every(interval, recur = true, &block)
64
65
  yield
65
66
  every(interval, recur, &block)
66
67
  end
67
-
68
+
68
69
  # Wait for the next timer and fire it. Can take a block, which should behave
69
70
  # like sleep(n), except that n may be nil (sleep forever) or a negative
70
71
  # number (fire immediately after return).
71
72
  def wait
72
73
  if block_given?
73
74
  yield wait_interval
74
-
75
+
75
76
  while (interval = wait_interval) && interval > 0
76
77
  yield interval
77
78
  end
@@ -84,7 +85,7 @@ module Timers
84
85
 
85
86
  fire
86
87
  end
87
-
88
+
88
89
  # Interval to wait until when the next timer will fire.
89
90
  # - nil: no timers
90
91
  # - -ve: timers expired already
@@ -94,36 +95,36 @@ module Timers
94
95
  handle = @events.first
95
96
  handle.time - Float(offset) if handle
96
97
  end
97
-
98
+
98
99
  # Fire all timers that are ready.
99
100
  def fire(offset = current_offset)
100
101
  @events.fire(offset)
101
102
  end
102
-
103
+
103
104
  # Pause all timers.
104
105
  def pause
105
106
  @timers.dup.each(&:pause)
106
107
  end
107
-
108
+
108
109
  # Resume all timers.
109
110
  def resume
110
111
  @paused_timers.dup.each(&:resume)
111
112
  end
112
-
113
+
113
114
  alias continue resume
114
-
115
+
115
116
  # Delay all timers.
116
117
  def delay(seconds)
117
118
  @timers.each do |timer|
118
119
  timer.delay(seconds)
119
120
  end
120
121
  end
121
-
122
+
122
123
  # Cancel all timers.
123
124
  def cancel
124
125
  @timers.dup.each(&:cancel)
125
126
  end
126
-
127
+
127
128
  # The group's current time.
128
129
  def current_offset
129
130
  @interval.to_f
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
- #
3
- # This file is part of the "timers" project and released under the MIT license.
4
- #
5
- # Copyright, 2018, by Samuel Williams. All rights reserved.
6
- #
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2018-2022, by Samuel Williams.
7
5
 
8
6
  module Timers
9
7
  # A collection of timers which may fire at different times
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2021, by Wander Hillen.
5
+ # Copyright, 2021-2022, by Samuel Williams.
6
+
7
+ module Timers
8
+ # A priority queue implementation using a standard binary minheap. It uses straight comparison
9
+ # of its contents to determine priority. This works because a Handle from Timers::Events implements
10
+ # the '<' operation by comparing the expiry time.
11
+ # See <https://en.wikipedia.org/wiki/Binary_heap> for explanations of the main methods.
12
+ class PriorityHeap
13
+ def initialize
14
+ # The heap is represented with an array containing a binary tree. See
15
+ # https://en.wikipedia.org/wiki/Binary_heap#Heap_implementation for how this array
16
+ # is built up.
17
+ @contents = []
18
+ end
19
+
20
+ # Returns the earliest timer or nil if the heap is empty.
21
+ def peek
22
+ @contents[0]
23
+ end
24
+
25
+ # Returns the number of elements in the heap
26
+ def size
27
+ @contents.size
28
+ end
29
+
30
+ # Returns the earliest timer if the heap is non-empty and removes it from the heap.
31
+ # Returns nil if the heap is empty. (and doesn't change the heap in that case)
32
+ def pop
33
+ # If the heap is empty:
34
+ if @contents.empty?
35
+ return nil
36
+ end
37
+
38
+ # If we have only one item, no swapping is required:
39
+ if @contents.size == 1
40
+ return @contents.pop
41
+ end
42
+
43
+ # Take the root of the tree:
44
+ value = @contents[0]
45
+
46
+ # Remove the last item in the tree:
47
+ last = @contents.pop
48
+
49
+ # Overwrite the root of the tree with the item:
50
+ @contents[0] = last
51
+
52
+ # Bubble it down into place:
53
+ bubble_down(0)
54
+
55
+ # validate!
56
+
57
+ return value
58
+ end
59
+
60
+ # Inserts a new timer into the heap, then rearranges elements until the heap invariant is true again.
61
+ def push(element)
62
+ # Insert the item at the end of the heap:
63
+ @contents.push(element)
64
+
65
+ # Bubble it up into position:
66
+ bubble_up(@contents.size - 1)
67
+
68
+ # validate!
69
+
70
+ return self
71
+ end
72
+
73
+ # Empties out the heap, discarding all elements
74
+ def clear!
75
+ @contents = []
76
+ end
77
+
78
+ # Validate the heap invariant. Every element except the root must not be smaller than
79
+ # its parent element. Note that it MAY be equal.
80
+ def valid?
81
+ # notice we skip index 0 on purpose, because it has no parent
82
+ (1..(@contents.size - 1)).all? { |e| @contents[e] >= @contents[(e - 1) / 2] }
83
+ end
84
+
85
+ private
86
+
87
+ def swap(i, j)
88
+ @contents[i], @contents[j] = @contents[j], @contents[i]
89
+ end
90
+
91
+ def bubble_up(index)
92
+ parent_index = (index - 1) / 2 # watch out, integer division!
93
+
94
+ while index > 0 && @contents[index] < @contents[parent_index]
95
+ # if the node has a smaller value than its parent, swap these nodes
96
+ # to uphold the minheap invariant and update the index of the 'current'
97
+ # node. If the node is already at index 0, we can also stop because that
98
+ # is the root of the heap.
99
+ # swap(index, parent_index)
100
+ @contents[index], @contents[parent_index] = @contents[parent_index], @contents[index]
101
+
102
+ index = parent_index
103
+ parent_index = (index - 1) / 2 # watch out, integer division!
104
+ end
105
+ end
106
+
107
+ def bubble_down(index)
108
+ swap_value = 0
109
+ swap_index = nil
110
+
111
+ while true
112
+ left_index = (2 * index) + 1
113
+ left_value = @contents[left_index]
114
+
115
+ if left_value.nil?
116
+ # This node has no children so it can't bubble down any further.
117
+ # We're done here!
118
+ return
119
+ end
120
+
121
+ # Determine which of the child nodes has the smallest value:
122
+ right_index = left_index + 1
123
+ right_value = @contents[right_index]
124
+
125
+ if right_value.nil? or right_value > left_value
126
+ swap_value = left_value
127
+ swap_index = left_index
128
+ else
129
+ swap_value = right_value
130
+ swap_index = right_index
131
+ end
132
+
133
+ if @contents[index] < swap_value
134
+ # No need to swap, the minheap invariant is already satisfied:
135
+ return
136
+ else
137
+ # 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:
138
+ # swap(index, swap_index)
139
+ @contents[index], @contents[swap_index] = @contents[swap_index], @contents[index]
140
+
141
+ index = swap_index
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
data/lib/timers/timer.rb CHANGED
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
- #
3
- # This file is part of the "timers" project and released under the MIT license.
4
- #
5
- # Copyright, 2018, by Samuel Williams. All rights reserved.
6
- #
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2014-2022, by Samuel Williams.
5
+ # Copyright, 2014-2017, by Tony Arcieri.
6
+ # Copyright, 2014, by Utenmiki.
7
+ # Copyright, 2014, by Lin Jen-Shin.
8
+ # Copyright, 2017, by Vít Ondruch.
7
9
 
8
10
  module Timers
9
11
  # An individual timer set to fire a given proc at a given time. A timer is
@@ -14,66 +16,66 @@ module Timers
14
16
  class Timer
15
17
  include Comparable
16
18
  attr_reader :interval, :offset, :recurring
17
-
19
+
18
20
  def initialize(group, interval, recurring = false, offset = nil, &block)
19
21
  @group = group
20
-
22
+
21
23
  @interval = interval
22
24
  @recurring = recurring
23
25
  @block = block
24
26
  @offset = offset
25
-
27
+
26
28
  @handle = nil
27
-
29
+
28
30
  # If a start offset was supplied, use that, otherwise use the current timers offset.
29
31
  reset(@offset || @group.current_offset)
30
32
  end
31
-
33
+
32
34
  def paused?
33
35
  @group.paused_timers.include? self
34
36
  end
35
-
37
+
36
38
  def pause
37
39
  return if paused?
38
-
40
+
39
41
  @group.timers.delete self
40
42
  @group.paused_timers.add self
41
-
43
+
42
44
  @handle.cancel! if @handle
43
45
  @handle = nil
44
46
  end
45
-
47
+
46
48
  def resume
47
49
  return unless paused?
48
-
50
+
49
51
  @group.paused_timers.delete self
50
-
52
+
51
53
  # This will add us back to the group:
52
54
  reset
53
55
  end
54
-
56
+
55
57
  alias continue resume
56
-
58
+
57
59
  # Extend this timer
58
60
  def delay(seconds)
59
61
  @handle.cancel! if @handle
60
-
62
+
61
63
  @offset += seconds
62
-
64
+
63
65
  @handle = @group.events.schedule(@offset, self)
64
66
  end
65
-
67
+
66
68
  # Cancel this timer. Do not call while paused.
67
69
  def cancel
68
70
  return unless @handle
69
-
71
+
70
72
  @handle.cancel! if @handle
71
73
  @handle = nil
72
-
74
+
73
75
  # This timer is no longer valid:
74
76
  @group.timers.delete self if @group
75
77
  end
76
-
78
+
77
79
  # Reset this timer. Do not call while paused.
78
80
  # @param offset [Numeric] the duration to add to the timer.
79
81
  def reset(offset = @group.current_offset)
@@ -84,12 +86,12 @@ module Timers
84
86
  else
85
87
  @group.timers << self
86
88
  end
87
-
89
+
88
90
  @offset = Float(offset) + @interval
89
-
91
+
90
92
  @handle = @group.events.schedule(@offset, self)
91
93
  end
92
-
94
+
93
95
  # Fire the block.
94
96
  def fire(offset = @group.current_offset)
95
97
  if recurring == :strict
@@ -100,36 +102,38 @@ module Timers
100
102
  else
101
103
  @offset = offset
102
104
  end
103
-
105
+
104
106
  @block.call(offset, self)
105
-
107
+
106
108
  cancel unless recurring
107
109
  end
108
-
110
+
109
111
  alias call fire
110
-
112
+
111
113
  # Number of seconds until next fire / since last fire
112
114
  def fires_in
113
115
  @offset - @group.current_offset if @offset
114
116
  end
115
-
117
+
116
118
  # Inspect a timer
117
119
  def inspect
118
- str = "#{to_s[0..-2]} ".dup
119
-
120
+ buffer = "#{to_s[0..-2]} ".dup
121
+
120
122
  if @offset
121
- str << if fires_in >= 0
122
- "fires in #{fires_in} seconds"
123
- else
124
- "fired #{fires_in.abs} seconds ago"
125
- end
126
-
127
- str << ", recurs every #{interval}" if recurring
123
+ if fires_in >= 0
124
+ buffer << "fires in #{fires_in} seconds"
125
+ else
126
+ buffer << "fired #{fires_in.abs} seconds ago"
127
+ end
128
+
129
+ buffer << ", recurs every #{interval}" if recurring
128
130
  else
129
- str << "dead"
131
+ buffer << "dead"
130
132
  end
131
-
132
- str << ">"
133
+
134
+ buffer << ">"
135
+
136
+ return buffer
133
137
  end
134
138
  end
135
139
  end
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
- #
3
- # This file is part of the "timers" project and released under the MIT license.
4
- #
5
- # Copyright, 2018, by Samuel Williams. All rights reserved.
6
- #
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2012-2016, by Tony Arcieri.
5
+ # Copyright, 2014-2022, by Samuel Williams.
6
+ # Copyright, 2015, by Donovan Keme.
7
7
 
8
8
  module Timers
9
- VERSION = "4.3.0"
9
+ VERSION = "4.3.5"
10
10
  end