wires 0.1.11 → 0.1.12

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
  SHA1:
3
- metadata.gz: d9c6a74afd25b600883e65a5bb6e54ac1e966eca
4
- data.tar.gz: 920c223d33dc8128fd0fc7f1b9af13992f5da51e
3
+ metadata.gz: 00826eb63c92ee41f33d4b9afca45cf79f6074ef
4
+ data.tar.gz: 7547996adc41b8f58905b22b50f317f403ef56b3
5
5
  SHA512:
6
- metadata.gz: 18d14b246f4f015b56e8fe9d0122201ecd27f6627764c62b814b1683aedfb705c5d1fb5b101432ec1f42a7dbdf0047916287d112a7555c94b81e851a478fe0e7
7
- data.tar.gz: ea363328dceda12c2c2b933be15018ddf16dd84125232718c7189c9a81562d01664dcf884b9ef31bdd0ba2eb3f333608a82c565f9f03eb9f9743dc4229ee26c5
6
+ metadata.gz: 64f55f73930e572d8fdb589a8c84f302396a87ff56ec851e0a6fe24b7666dd8f572ee6638169a0571728e6364ea9b85d38d6d843b133656690cd22072cd00e8c
7
+ data.tar.gz: efac20e8fc999a1e219ff778ee92cbeb0c5ebfd2e520708b382bd86411a6133c6ceac8ab7084620439e6c8ae4a4ff98bdece2136aa3f4bcc9a73dc3bb968bf71
@@ -1,105 +1,107 @@
1
1
 
2
2
  def on(events, channels='*', &codeblock)
3
- channels = [channels] unless channels.is_a? Array
4
- for channel in channels
5
- Channel(channel).register(events, codeblock)
6
- end
3
+ channels = [channels] unless channels.is_a? Array
4
+ for channel in channels
5
+ Channel.new(channel).register(events, codeblock)
6
+ end
7
7
  nil end
8
8
 
9
9
 
10
10
  def fire(event, channel='*')
11
- Channel(channel).fire(event, blocking=false)
11
+ Channel.new(channel).fire(event, blocking:false)
12
12
  nil end
13
13
 
14
14
  def fire_and_wait(event, channel='*')
15
- Channel(channel).fire(event, blocking=true)
15
+ Channel.new(channel).fire(event, blocking:true)
16
16
  nil end
17
17
 
18
18
 
19
19
  def Channel(*args) Channel.new(*args) end
20
20
 
21
21
  class Channel
22
+
23
+ attr_reader :name
24
+ attr_reader :target_list
25
+
26
+ def initialize(name)
27
+ @name = name
28
+ @target_list = Set.new
29
+ nil end
30
+
31
+
32
+ # Redefine this class method to use an alternate Hub
33
+ def self.hub; Hub; end
34
+ # Don't redefine this instance method!
35
+ def hub; self.class.hub; end
36
+
37
+ # Channel registry hash and star channel reference are values
38
+ # In this Hash with the key being the reference to the Hub
39
+ @@channel_hash = Hash.new
40
+ @@channel_star = Hash.new
41
+
42
+ # Give out references to the star channel
43
+ def self.channel_star; @@channel_star[self.hub]; end
44
+ def channel_star; @@channel_star[self.hub]; end
45
+
46
+ # Ensure that there is only one instance of Channel per name
47
+ @@new_lock = Mutex.new
48
+ def self.new(*args, &block)
49
+ @@channel_star[self.hub] ||= Channel.new('*') unless (args[0]=='*')
50
+ @@new_lock.synchronize do
51
+ @@channel_hash[self.hub] ||= Hash.new
52
+ @@channel_hash[self.hub][args[0]] ||= super(*args, &block)
53
+ end
54
+ end
55
+
56
+ # Register a proc to be triggered by an event on this channel
57
+ def register(events, proc)
22
58
 
23
- attr_reader :name
24
- attr_reader :target_list
59
+ if not proc.is_a?(Proc) then raise SyntaxError, \
60
+ "No Proc given to execute on event: #{events}" end
25
61
 
26
- def initialize(name)
27
- @name = name
28
- @target_list = Set.new
29
- nil end
62
+ # Convert all events to strings
63
+ events = [events] unless events.is_a? Array
64
+ events.flatten!
65
+ events.map! { |e| (e.is_a?(Class) ? e.codestring : e.to_s) }
66
+ events.uniq!
30
67
 
31
- # Ensure that there is only one instance of Channel per name
32
- @@channel_hash = Hash.new
33
- @@new_lock = Mutex.new
34
- def self.new(*args, &block)
35
- @@new_lock.synchronize do
36
- @@channel_hash[args[0]] ||= super(*args, &block)
37
- end
38
- end
68
+ @target_list << [events, proc]
69
+ nil end
70
+
71
+ # Fire an event on this channel
72
+ def fire(event, blocking:false)
39
73
 
40
- # Class-wide reference to the global channel and event hub
41
- @@channel_star = Channel('*')
74
+ # Create an instance object from one of several acceptable input forms
75
+ event = Event.new_from event
42
76
 
43
- # Register a proc to be triggered by an event on this channel
44
- def register(events, proc)
45
-
46
- if not proc.is_a?(Proc) then raise SyntaxError, \
47
- "No Proc given to execute on event: #{events}" end
48
-
49
- # Convert all events to strings
50
- events = [events] unless events.is_a? Array
51
- events.flatten!
52
- events.map! { |e| (e.is_a?(Class) ? e.codestring : e.to_s) }
53
- events.uniq!
54
-
55
- @target_list << [events, proc]
56
- nil end
77
+ # Fire to each relevant target on each channel
78
+ for chan in relevant_channels()
79
+ for target in chan.target_list
80
+ for string in target[0] & event.class.codestrings
81
+ self.class.hub << [string, event, blocking, *target[1..-1]]
82
+ end end end
57
83
 
58
- # Fire an event on this channel
59
- def fire(_event, blocking=false)
60
-
61
- # Pull out args from optional array notation
62
- _event = [_event] unless _event.is_a? Array
63
- _event, *args = _event
64
-
65
- # Create event object from event as an object, class, or symbol/string
66
- event = case _event
67
- when Event
68
- _event
69
- when Class
70
- _event.new(*args) if _event < Event
71
- else
72
- cls = Event.from_codestring(_event.to_s).new(*args)
73
- end
74
-
75
- # Fire to each relevant target on each channel
76
- for chan in relevant_channels()
77
- for target in chan.target_list
78
- for string in target[0] & event.class.codestrings
79
- Hub << [string, event, blocking, *target[1..-1]]
80
- end end end
81
-
82
- nil end
84
+ nil end
85
+
86
+ def relevant_channels
87
+ return @@channel_hash[hub].values if self==channel_star
83
88
 
84
- def relevant_channels
85
- return @@channel_hash.values if self==@@channel_star
86
-
87
- if self.name.is_a?(Regexp) then raise TypeError,
88
- "Cannot fire on Regexp channel: #{self.name}."\
89
- " Regexp channels can only used in event handlers." end
90
-
91
- relevant = [@@channel_star]
92
- for c in @@channel_hash.values
93
- relevant << c if \
94
- if c.name.is_a?(Regexp)
95
- self.name =~ c.name
96
- elsif (defined?(c.name.channel_name) and
97
- defined?(self.name.channel_name))
98
- self.name.channel_name == c.name.channel_name
99
- else
100
- self.name.to_s == c.name.to_s
101
- end
89
+ if self.name.is_a?(Regexp) then raise TypeError,
90
+ "Cannot fire on Regexp channel: #{self.name}."\
91
+ " Regexp channels can only used in event handlers." end
92
+
93
+ relevant = [channel_star]
94
+ for c in @@channel_hash[hub].values
95
+ relevant << c if \
96
+ if c.name.is_a?(Regexp)
97
+ self.name =~ c.name
98
+ elsif (defined?(c.name.channel_name) and
99
+ defined?(self.name.channel_name))
100
+ self.name.channel_name == c.name.channel_name
101
+ else
102
+ self.name.to_s == c.name.to_s
102
103
  end
103
- return relevant.uniq
104
104
  end
105
+ return relevant.uniq
106
+ end
105
107
  end
data/lib/wires/events.rb CHANGED
@@ -69,7 +69,26 @@ class Event < Object # explicit for the sake of Event.ancestry
69
69
  "No known Event subclass with codestring: '#{str}'" end
70
70
  cls
71
71
  end
72
+
73
+ # Convert an event from 'array notation' to an Event subclass instance
74
+ # TODO: List acceptable input forms here for documentation
75
+ def new_from(input)
76
+
77
+ # Standardize to array and pull out arguments if they exist
78
+ input = [input] unless input.is_a? Array
79
+ input, *args = input
72
80
 
81
+ # Create event object from event as an object, class, or symbol/string
82
+ event = case input
83
+ when Event
84
+ input
85
+ when Class
86
+ input.new(*args) if input < Event
87
+ else
88
+ Event.from_codestring(input.to_s).new(*args)
89
+ end
90
+ end
91
+
73
92
  # Create attributes and accessors for all arguments to the constructor.
74
93
  # This is done here rather than in initialize so that the functionality
75
94
  # will remain if the user developer overrides initialize in the subclass.
@@ -0,0 +1,6 @@
1
+
2
+ def expect_type(x, type)
3
+ unless x.is_a? type
4
+ raise "Expected #{x.inspect} to be an instance of #{type}."
5
+ end
6
+ end
data/lib/wires/hub.rb CHANGED
@@ -62,25 +62,21 @@ class Hub
62
62
  # [+:blocking+] calling thread won't be done until Hub thread is done
63
63
  def kill(*flags)
64
64
  @finish_all = (flags.include? :finish_all)
65
- @state=:dying if alive?
66
- @thread.join if (flags.include? :blocking)
65
+ @state=:dying
66
+ @thread.join if (dying? and flags.include? :blocking)
67
67
  nil end
68
68
 
69
69
  # Register hook to execute before kill - can call multiple times
70
70
  def before_kill(proc=nil, retain:false, &block)
71
71
  func = (block or proc)
72
- if not func.is_a?(Proc)
73
- raise TypeError, "Expected a Proc or code block to execute."
74
- end
72
+ expect_type func, Proc
75
73
  @before_kills << [func, retain]
76
74
  nil end
77
75
 
78
76
  # Register hook to execute after kill - can call multiple times
79
77
  def after_kill(proc=nil, retain:false, &block)
80
78
  func = (block or proc)
81
- if not func.is_a?(Proc)
82
- raise TypeError, "Expected a Proc or code block to execute."
83
- end
79
+ expect_type func, Proc
84
80
  @after_kills << [func, retain]
85
81
  nil end
86
82
 
@@ -195,6 +191,7 @@ class Hub
195
191
  unhandled_exception(e)
196
192
  end
197
193
  end
194
+
198
195
  end
199
196
 
200
197
  nil end
data/lib/wires/time.rb CHANGED
@@ -1,45 +1,137 @@
1
- require 'pry'
2
1
 
3
- class TimeSchedulerEvent < Event; end
4
- class TimeSchedulerStartEvent < TimeSchedulerEvent; end
5
- class TimeSchedulerAnonEvent < TimeSchedulerEvent; end
2
+ class TimeSchedulerStartEvent < Event; end
3
+ class TimeSchedulerAnonEvent < Event; end
4
+
5
+
6
+ class TimeSchedulerItem
7
+
8
+ attr_reader :time, :event, :channel, :interval
9
+
10
+ def initialize(time, event, channel='*',
11
+ interval:0.seconds, count:1,
12
+ ignore_past:false, cancel:false)
13
+
14
+ expect_type time, Time
15
+
16
+ @active = (not cancel)
17
+ tempcount = count
18
+
19
+ while (time < Time.now) and (tempcount > 0)
20
+ time += interval
21
+ tempcount -= 1
22
+ end
23
+ if not ignore_past
24
+ time -= interval
25
+ self.count = count
26
+ else
27
+ self.count = tempcount
28
+ end
29
+
30
+ @time = time
31
+ @event = Event.new_from(event)
32
+ @channel = channel
33
+ @interval = interval
34
+
35
+ end
36
+
37
+ def active?; @active end
38
+ def inactive?; not @active end
39
+ def ready?; @active and (Time.now >= @time) end
40
+ def time_until; (@active ? [(Time.now - @time), 0].max : nil) end
41
+
42
+ def cancel; @active = false ;nil end
43
+
44
+ # Get/set @count (and apply constraints on set)
45
+ def count; @count end
46
+ #TODO: handle explicit cancel?
47
+ def count=(x); @count=[x,0].max; @active&&=(count>0) ;nil end
48
+
49
+ # Inc/dec @count. Necessary because += and -= outside of lock are not atomic!
50
+ def count_inc(x=1); self.count=(@count+x) end
51
+ def count_dec(x=1); self.count=(@count-x) end
52
+
53
+ # Fire the event now, regardless of time or active status
54
+ def fire(*args)
55
+ Channel.new(@channel).fire(@event, *args)
56
+ count_dec
57
+ @time += @interval if @active
58
+ nil end
59
+
60
+ # Fire the event only if it is ready
61
+ def fire_if_ready(*args); self.fire(*args) if ready? end
62
+
63
+ # Block until event is ready
64
+ def wait_until_ready; sleep 0 until ready? end
65
+
66
+ # Block until event is ready, then fire and block until it is done
67
+ def fire_when_ready(*args);
68
+ wait_until_ready
69
+ fire_if_ready(*args)
70
+ end
71
+
72
+ # Lock (almost) all instance methods with common re-entrant lock
73
+ threadlock instance_methods-superclass.instance_methods-[
74
+ :block_until_ready,
75
+ :fire_when_ready]
76
+ end
6
77
 
7
78
  # A singleton class to schedule future firing of events
8
79
  class TimeScheduler
9
80
  @schedule = Array.new
10
- @schedule_lock = Mutex.new
11
81
  @thread = Thread.new {nil}
12
- @keepgoing_lock = Mutex.new
82
+ @schedule_lock = Monitor.new
83
+ @dont_sleep = false
13
84
 
14
85
  # Operate on the metaclass as a type of singleton pattern
15
86
  class << self
16
87
 
88
+ # Add an event to the schedule
89
+ def add(new_item)
90
+ expect_type new_item, TimeSchedulerItem
91
+ schedule_add(new_item)
92
+ wakeup
93
+ nil end
94
+ # Add an event to the schedule using << operator
95
+ alias_method :<<, :add
96
+
17
97
  # Get a copy of the event schedule from outside the class
18
- def list; @schedule_lock.synchronize {@schedule.clone} end
98
+ def list; @schedule.clone end
19
99
  # Clear the event schedule from outside the class
20
- def clear; @schedule_lock.synchronize {@schedule.clear} end
100
+ def clear; @schedule.clear end
21
101
 
22
- # Fire an event at a specific time
23
- def fire(time, event, channel='*', ignore_past:false)
24
- if not time.is_a? Time
25
- raise TypeError, "Expected #{time.inspect} to be an instance of Time."
26
- end
27
-
28
- # Ignore past events if flag is set
29
- if ignore_past and time < Time.now; return nil; end
30
-
31
- # Under mutex, push the event into the schedule and sort
32
- @schedule_lock.synchronize do
33
- @schedule << {time:time, event:event, channel:channel}
34
- @schedule.sort! { |a,b| a[:time] <=> b[:time] }
35
- end
36
-
37
- # Wakeup main_loop thread if it is sleeping
38
- begin @thread.wakeup; rescue ThreadError; end
39
-
102
+ private
103
+
104
+ def schedule_reshuffle
105
+ @schedule.select! {|x| x.active?}
106
+ @schedule.sort! {|a,b| a.time <=> b.time}
40
107
  nil end
41
108
 
42
- private
109
+ def schedule_add(new_item)
110
+ @schedule << new_item
111
+ schedule_reshuffle
112
+ nil end
113
+
114
+ def schedule_concat(other_list)
115
+ @schedule.concat other_list
116
+ schedule_reshuffle
117
+ nil end
118
+
119
+ def schedule_pull
120
+ pending = Array.new
121
+ while ((not @schedule.empty?) and @schedule[0].ready?)
122
+ pending << @schedule.shift
123
+ end
124
+ [pending, @schedule[0]]
125
+ end
126
+
127
+ # Put all functions dealing with @schedule under @schedule_lock
128
+ threadlock :list,
129
+ :clear,
130
+ :schedule_reshuffle,
131
+ :schedule_add,
132
+ :schedule_concat,
133
+ :schedule_pull,
134
+ lock: :@schedule_lock
43
135
 
44
136
  # Do scheduled firing of events as long as Hub is alive
45
137
  def main_loop
@@ -51,60 +143,56 @@ class TimeScheduler
51
143
 
52
144
  while @keepgoing
53
145
 
54
- # Under mutex, pull any events that are ready into pending
55
- pending.clear
56
- @schedule_lock.synchronize do
57
- while ((not @schedule.empty?) and
58
- (Time.now > @schedule[0][:time]))
59
- pending << @schedule.shift
60
- end
61
- on_deck = @schedule[0]
62
- end
63
-
64
- # Fire pending events
65
- pending.each { |x| Channel(x[:channel]).fire(x[:event]) }
146
+ # Pull, fire, and requeue relevant events
147
+ pending, on_deck = schedule_pull
148
+ pending.each { |x| x.fire }
149
+ schedule_concat pending
66
150
 
151
+ @sleepzone = true
67
152
  # Calculate the time to sleep based on next event's time
68
153
  if on_deck
69
- sleep [(on_deck[:time]-Time.now), 0].max
154
+ sleep on_deck.time_until
70
155
  else # sleep until wakeup if no event is on deck
71
- sleep if @keepgoing
156
+ sleep
72
157
  end
73
-
158
+ @sleepzone = false
74
159
  end
75
160
 
76
161
  nil end
77
162
 
163
+ def wakeup
164
+ sleep 0 until @sleepzone==true
165
+ sleep 0 until @thread.status=='sleep'
166
+ @thread.wakeup
167
+ nil end
78
168
 
79
169
  end
80
170
 
81
171
  # Use fired event to only start scheduler when Hub is running
82
172
  # This also gets the scheduler loop its own thread within the Hub's threads
83
173
  on :time_scheduler_start, self do; main_loop; end;
84
- Channel(self).fire(:time_scheduler_start)
174
+ Channel.new(self).fire(:time_scheduler_start)
85
175
 
86
176
  # Stop the main loop upon death of Hub
87
177
  Hub.before_kill(retain:true) do
88
- @keepgoing_lock.synchronize do
89
- @keepgoing=false
90
- @thread.wakeup
91
- end
92
- sleep 0
178
+ sleep 0 until @sleepzone==true
179
+ @keepgoing=false
180
+ wakeup
93
181
  end
182
+
94
183
  # Refire the start event after Hub dies in case it restarts
95
184
  Hub.after_kill(retain:true) do
96
- Channel(self).fire(:time_scheduler_start)
185
+ Channel.new(self).fire(:time_scheduler_start)
97
186
  end
98
187
 
99
-
100
188
  end
101
189
 
102
190
 
103
191
  # Reopen the Time class and add the fire method to enable nifty syntax like:
104
192
  # 32.minutes.from_now.fire :event
105
193
  class Time
106
- def fire(*args)
107
- TimeScheduler.fire(self, *args)
194
+ def fire(event, channel='*', **kwargs)
195
+ TimeScheduler << TimeSchedulerItem.new(self, event, channel, **kwargs)
108
196
  end
109
197
  end
110
198
 
@@ -116,7 +204,7 @@ class ActiveSupport::Duration
116
204
  alias :__original_since :since
117
205
  def since(*args, &block)
118
206
  if block
119
- on :time_scheduler_anon, block.object_id do block.call end
207
+ on :time_scheduler_anon, block.object_id do |e| block.call(e) end
120
208
  __original_since(*args).fire(:time_scheduler_anon,
121
209
  block.object_id)
122
210
  nil
@@ -129,7 +217,7 @@ class ActiveSupport::Duration
129
217
  alias :__original_ago :ago
130
218
  def ago(*args, &block)
131
219
  if block
132
- on :time_scheduler_anon, block.object_id do block.call end
220
+ on :time_scheduler_anon, block.object_id do |e| block.call(e) end
133
221
  __original_ago(*args).fire(:time_scheduler_anon,
134
222
  block.object_id)
135
223
  nil
@@ -139,4 +227,8 @@ class ActiveSupport::Duration
139
227
  end
140
228
  alias :until :ago
141
229
 
142
- end
230
+ end
231
+
232
+
233
+ # TODO: Repeatable event sugar?
234
+ # TODO: Tests for all new functionality
data/lib/wires.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  require 'set'
2
2
  require 'thread'
3
3
  require 'active_support/core_ext' # Convenience functions from Rails
4
+ require 'threadlock' # Easily add re-entrant lock to instance methods
4
5
 
6
+ require 'wires/expect_type'
5
7
  require 'wires/events'
6
8
  require 'wires/hub'
7
9
  require 'wires/channels'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wires
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.11
4
+ version: 0.1.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joe McIlvain
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-07-01 00:00:00.000000000 Z
11
+ date: 2013-07-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - '>='
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: threadlock
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  description: An asynchronous (threaded) event routing framework in Ruby. Patch your
28
42
  objects together with wires. Inspired by the python 'circuits' framework.
29
43
  email: joe.eli.mac@gmail.com
@@ -34,6 +48,7 @@ files:
34
48
  - lib/wires.rb
35
49
  - lib/wires/time.rb
36
50
  - lib/wires/hub.rb
51
+ - lib/wires/expect_type.rb
37
52
  - lib/wires/events.rb
38
53
  - lib/wires/channels.rb
39
54
  - LICENSE