wires 0.1.11 → 0.1.12

Sign up to get free protection for your applications and to get access to all the features.
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