timers 4.2.0 → 4.3.3

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: e1665f074c230a801e0eb6648214a6d0248d4baada8da1f4e60abd0983bba517
4
- data.tar.gz: de4eac12ecba1826d6ae5ef1d6958a451595b1a9e8e54dba7f1718d7f15182dd
3
+ metadata.gz: a9f471bfb9cac57f9462498ab12527eded7533318cec13604a10d32b159c6689
4
+ data.tar.gz: 69802b0774101037596274f2acf114c3f4234875ed8a82146e8ef1075407bf6d
5
5
  SHA512:
6
- metadata.gz: c0e005f02975d324b7fbe7d8e284a2d5318be78352e129ce00abf20594d3e6a18b30103e1137b2d9fe91bd5a2251e7b22fb5cdc255f5153f219bf90bd3b6160d
7
- data.tar.gz: 64210a85d0a8efe2540d9ee6da050c5d8298a5332bd2ba14758bd784a7f991ad40b1f828b47320525ab025249859ca6419f172421492fe6ced56a76e34820541
6
+ metadata.gz: f15934803658ec4b1d0d7b4a8ea92c9a21e2267544ec11c9619604288264c7f0add19dd12b71a0680827d6d37b853192d98266f29b06ef27cb36e53408f8c67b
7
+ data.tar.gz: a50a935d7b73681b4b751341c6eda0143c64beb9d469641ea68b8481cd1c36bd16ad21f7ec274f06d1389b53c5849e06b20182d3faea6497f64773d8fba2dae6
data/lib/timers.rb CHANGED
@@ -1,11 +1,26 @@
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
- #
7
2
 
8
- require "timers/version"
3
+ # Copyright, 2020, by Samuel G. D. Williams. <http://www.codeotaku.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
9
22
 
10
- require "timers/group"
11
- require "timers/wait"
23
+ require_relative "timers/version"
24
+
25
+ require_relative "timers/group"
26
+ require_relative "timers/wait"
data/lib/timers/events.rb CHANGED
@@ -1,113 +1,115 @@
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
+ # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
7
22
 
8
23
  require_relative "timer"
24
+ require_relative "priority_heap"
9
25
 
10
26
  module Timers
11
- # Maintains an ordered list of events, which can be cancelled.
27
+ # Maintains a PriorityHeap of events ordered on time, which can be cancelled.
12
28
  class Events
13
29
  # Represents a cancellable handle for a specific timer event.
14
30
  class Handle
31
+ include Comparable
32
+
15
33
  def initialize(time, callback)
16
34
  @time = time
17
35
  @callback = callback
18
36
  end
19
-
37
+
20
38
  # The absolute time that the handle should be fired at.
21
39
  attr_reader :time
22
-
40
+
23
41
  # Cancel this timer, O(1).
24
42
  def cancel!
25
43
  # The simplest way to keep track of cancelled status is to nullify the
26
44
  # callback. This should also be optimal for garbage collection.
27
45
  @callback = nil
28
46
  end
29
-
47
+
30
48
  # Has this timer been cancelled? Cancelled timer's don't fire.
31
49
  def cancelled?
32
50
  @callback.nil?
33
51
  end
34
-
35
- def >(other)
36
- @time > other.to_f
37
- end
38
-
39
- def to_f
40
- @time
52
+
53
+ def <=> other
54
+ @time <=> other.time
41
55
  end
42
-
56
+
43
57
  # Fire the callback if not cancelled with the given time parameter.
44
58
  def fire(time)
45
59
  @callback.call(time) if @callback
46
60
  end
47
61
  end
48
-
62
+
49
63
  def initialize
50
64
  # A sequence of handles, maintained in sorted order, future to present.
51
65
  # @sequence.last is the next event to be fired.
52
- @sequence = []
66
+ @sequence = PriorityHeap.new
67
+ @queue = []
53
68
  end
54
-
69
+
55
70
  # Add an event at the given time.
56
71
  def schedule(time, callback)
57
72
  handle = Handle.new(time.to_f, callback)
58
-
59
- index = bisect_left(@sequence, handle)
60
-
61
- # Maintain sorted order, O(logN) insertion time.
62
- @sequence.insert(index, handle)
63
-
64
- handle
73
+
74
+ @queue << handle
75
+
76
+ return handle
65
77
  end
66
-
78
+
67
79
  # Returns the first non-cancelled handle.
68
80
  def first
69
- while (handle = @sequence.last)
81
+ merge!
82
+
83
+ while (handle = @sequence.peek)
70
84
  return handle unless handle.cancelled?
71
85
  @sequence.pop
72
86
  end
73
87
  end
74
-
88
+
75
89
  # Returns the number of pending (possibly cancelled) events.
76
90
  def size
77
- @sequence.size
91
+ @sequence.size + @queue.size
78
92
  end
79
-
93
+
80
94
  # Fire all handles for which Handle#time is less than the given time.
81
95
  def fire(time)
82
- pop(time).reverse_each do |handle|
96
+ merge!
97
+
98
+ while handle = @sequence.peek and handle.time <= time
99
+ @sequence.pop
83
100
  handle.fire(time)
84
101
  end
85
102
  end
86
-
103
+
87
104
  private
88
-
89
- # Efficiently take k handles for which Handle#time is less than the given
90
- # time.
91
- def pop(time)
92
- index = bisect_left(@sequence, time)
93
-
94
- @sequence.pop(@sequence.size - index)
95
- end
96
-
97
- # Return the left-most index where to insert item e, in a list a, assuming
98
- # a is sorted in descending order.
99
- def bisect_left(a, e, l = 0, u = a.length)
100
- while l < u
101
- m = l + (u - l).div(2)
102
-
103
- if a[m] > e
104
- l = m + 1
105
- else
106
- u = m
107
- end
105
+
106
+ # Move all non-cancelled timers from the pending queue to the priority heap
107
+ def merge!
108
+ while handle = @queue.pop
109
+ next if handle.cancelled?
110
+
111
+ @sequence.push(handle)
108
112
  end
109
-
110
- l
111
113
  end
112
114
  end
113
115
  end
data/lib/timers/group.rb CHANGED
@@ -1,9 +1,24 @@
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
+ # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
7
22
 
8
23
  require "set"
9
24
  require "forwardable"
@@ -16,62 +31,62 @@ module Timers
16
31
  # A collection of timers which may fire at different times
17
32
  class Group
18
33
  include Enumerable
19
-
34
+
20
35
  extend Forwardable
21
36
  def_delegators :@timers, :each, :empty?
22
-
37
+
23
38
  def initialize
24
39
  @events = Events.new
25
-
40
+
26
41
  @timers = Set.new
27
42
  @paused_timers = Set.new
28
-
43
+
29
44
  @interval = Interval.new
30
45
  @interval.start
31
46
  end
32
-
47
+
33
48
  # Scheduled events:
34
49
  attr_reader :events
35
-
50
+
36
51
  # Active timers:
37
52
  attr_reader :timers
38
-
53
+
39
54
  # Paused timers:
40
55
  attr_reader :paused_timers
41
-
56
+
42
57
  # Call the given block after the given interval. The first argument will be
43
58
  # the time at which the group was asked to fire timers for.
44
59
  def after(interval, &block)
45
60
  Timer.new(self, interval, false, &block)
46
61
  end
47
-
62
+
48
63
  # Call the given block immediately, and then after the given interval. The first
49
64
  # argument will be the time at which the group was asked to fire timers for.
50
65
  def now_and_after(interval, &block)
51
66
  yield
52
67
  after(interval, &block)
53
68
  end
54
-
69
+
55
70
  # Call the given block periodically at the given interval. The first
56
71
  # argument will be the time at which the group was asked to fire timers for.
57
72
  def every(interval, recur = true, &block)
58
73
  Timer.new(self, interval, recur, &block)
59
74
  end
60
-
75
+
61
76
  # Call the given block immediately, and then periodically at the given interval. The first
62
77
  # argument will be the time at which the group was asked to fire timers for.
63
78
  def now_and_every(interval, recur = true, &block)
64
79
  yield
65
80
  every(interval, recur, &block)
66
81
  end
67
-
82
+
68
83
  # Wait for the next timer and fire it. Can take a block, which should behave
69
84
  # like sleep(n), except that n may be nil (sleep forever) or a negative
70
85
  # number (fire immediately after return).
71
86
  def wait
72
87
  if block_given?
73
88
  yield wait_interval
74
-
89
+
75
90
  while (interval = wait_interval) && interval > 0
76
91
  yield interval
77
92
  end
@@ -81,10 +96,10 @@ module Timers
81
96
  sleep interval
82
97
  end
83
98
  end
84
-
99
+
85
100
  fire
86
101
  end
87
-
102
+
88
103
  # Interval to wait until when the next timer will fire.
89
104
  # - nil: no timers
90
105
  # - -ve: timers expired already
@@ -94,36 +109,36 @@ module Timers
94
109
  handle = @events.first
95
110
  handle.time - Float(offset) if handle
96
111
  end
97
-
112
+
98
113
  # Fire all timers that are ready.
99
114
  def fire(offset = current_offset)
100
115
  @events.fire(offset)
101
116
  end
102
-
117
+
103
118
  # Pause all timers.
104
119
  def pause
105
120
  @timers.dup.each(&:pause)
106
121
  end
107
-
122
+
108
123
  # Resume all timers.
109
124
  def resume
110
125
  @paused_timers.dup.each(&:resume)
111
126
  end
112
-
127
+
113
128
  alias continue resume
114
-
129
+
115
130
  # Delay all timers.
116
131
  def delay(seconds)
117
132
  @timers.each do |timer|
118
133
  timer.delay(seconds)
119
134
  end
120
135
  end
121
-
136
+
122
137
  # Cancel all timers.
123
138
  def cancel
124
139
  @timers.dup.each(&:cancel)
125
140
  end
126
-
141
+
127
142
  # The group's current time.
128
143
  def current_offset
129
144
  @interval.to_f
@@ -1,9 +1,24 @@
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
+ # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
7
22
 
8
23
  module Timers
9
24
  # A collection of timers which may fire at different times
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright, 2021, by Wander Hillen.
4
+ # Copyright, 2021, by Samuel G. D. Williams. <http://www.codeotaku.com>
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ # THE SOFTWARE.
23
+
24
+ module Timers
25
+ # A priority queue implementation using a standard binary minheap. It uses straight comparison
26
+ # of its contents to determine priority. This works because a Handle from Timers::Events implements
27
+ # the '<' operation by comparing the expiry time.
28
+ # See <https://en.wikipedia.org/wiki/Binary_heap> for explanations of the main methods.
29
+ class PriorityHeap
30
+ def initialize
31
+ # The heap is represented with an array containing a binary tree. See
32
+ # https://en.wikipedia.org/wiki/Binary_heap#Heap_implementation for how this array
33
+ # is built up.
34
+ @contents = []
35
+ end
36
+
37
+ # Returns the earliest timer or nil if the heap is empty.
38
+ def peek
39
+ @contents[0]
40
+ end
41
+
42
+ # Returns the number of elements in the heap
43
+ def size
44
+ @contents.size
45
+ end
46
+
47
+ # Returns the earliest timer if the heap is non-empty and removes it from the heap.
48
+ # Returns nil if the heap is empty. (and doesn't change the heap in that case)
49
+ def pop
50
+ # If the heap is empty:
51
+ if @contents.empty?
52
+ return nil
53
+ end
54
+
55
+ # If we have only one item, no swapping is required:
56
+ if @contents.size == 1
57
+ return @contents.pop
58
+ end
59
+
60
+ # Take the root of the tree:
61
+ value = @contents[0]
62
+
63
+ # Remove the last item in the tree:
64
+ last = @contents.pop
65
+
66
+ # Overwrite the root of the tree with the item:
67
+ @contents[0] = last
68
+
69
+ # Bubble it down into place:
70
+ bubble_down(0)
71
+
72
+ # validate!
73
+
74
+ return value
75
+ end
76
+
77
+ # Inserts a new timer into the heap, then rearranges elements until the heap invariant is true again.
78
+ def push(element)
79
+ # Insert the item at the end of the heap:
80
+ @contents.push(element)
81
+
82
+ # Bubble it up into position:
83
+ bubble_up(@contents.size - 1)
84
+
85
+ # validate!
86
+
87
+ return self
88
+ end
89
+
90
+ private
91
+
92
+ # Validate the heap invariant.
93
+ def validate!(index = 0)
94
+ if value = @contents[index]
95
+ left_index = index*2 + 1
96
+ if left_value = @contents[left_index]
97
+ unless value < left_value
98
+ raise "Invalid left index from #{index}!"
99
+ end
100
+
101
+ validate!(left_index)
102
+ end
103
+
104
+ right_index = left_index + 1
105
+ if right_value = @contents[right_index]
106
+ unless value < right_value
107
+ raise "Invalid right index from #{index}!"
108
+ end
109
+
110
+ validate!(right_index)
111
+ end
112
+ end
113
+ end
114
+
115
+ def swap(i, j)
116
+ @contents[i], @contents[j] = @contents[j], @contents[i]
117
+ end
118
+
119
+ def bubble_up(index)
120
+ parent_index = (index - 1) / 2 # watch out, integer division!
121
+
122
+ while index > 0 && @contents[index] < @contents[parent_index]
123
+ # if the node has a smaller value than its parent, swap these nodes
124
+ # to uphold the minheap invariant and update the index of the 'current'
125
+ # node. If the node is already at index 0, we can also stop because that
126
+ # is the root of the heap.
127
+ # swap(index, parent_index)
128
+ @contents[index], @contents[parent_index] = @contents[parent_index], @contents[index]
129
+
130
+ index = parent_index
131
+ parent_index = (index - 1) / 2 # watch out, integer division!
132
+ end
133
+ end
134
+
135
+ def bubble_down(index)
136
+ swap_value = 0
137
+ swap_index = nil
138
+
139
+ while true
140
+ left_index = (2 * index) + 1
141
+ left_value = @contents[left_index]
142
+
143
+ if left_value.nil?
144
+ # This node has no children so it can't bubble down any further.
145
+ # We're done here!
146
+ return
147
+ end
148
+
149
+ # Determine which of the child nodes has the smallest value:
150
+ right_index = left_index + 1
151
+ right_value = @contents[right_index]
152
+
153
+ if right_value.nil? or right_value > left_value
154
+ swap_value = left_value
155
+ swap_index = left_index
156
+ else
157
+ swap_value = right_value
158
+ swap_index = right_index
159
+ end
160
+
161
+ if @contents[index] < swap_value
162
+ # No need to swap, the minheap invariant is already satisfied:
163
+ return
164
+ else
165
+ # 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:
166
+ # swap(index, swap_index)
167
+ @contents[index], @contents[swap_index] = @contents[swap_index], @contents[index]
168
+
169
+ index = swap_index
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end