tilia-event 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,169 @@
1
+ module Tilia
2
+ module Event
3
+ # Event Emitter Trait
4
+ #
5
+ # This trait contains all the basic functions to implement an
6
+ # EventEmitterInterface.
7
+ #
8
+ # Using the trait + interface allows you to add EventEmitter capabilities
9
+ # without having to change your base-class.
10
+ module EventEmitterTrait
11
+ # The list of listeners
12
+ #
13
+ # @return [Hash]
14
+ attr_accessor :listeners
15
+
16
+ # Subscribe to an event.
17
+ #
18
+ # @param [String] event_name
19
+ # @param [Proc, Method] call_back
20
+ # @param [Fixnum] priority
21
+ # @return [void]
22
+ def on(event_name, call_back, priority = 100)
23
+ @listeners[event_name] ||= [false, [], []]
24
+
25
+ @listeners[event_name][0] = @listeners[event_name][1].size == 0
26
+ @listeners[event_name][1] << priority
27
+ @listeners[event_name][2] << call_back
28
+ end
29
+
30
+ # Subscribe to an event exactly once.
31
+ #
32
+ # @param [String] event_name
33
+ # @param [Proc, Method] call_back
34
+ # @param [Fixnum] priority
35
+ # @return [void]
36
+ def once(event_name, call_back, priority = 100)
37
+ wrapper = nil
38
+ wrapper = lambda do |*arguments|
39
+ remove_listener(event_name, wrapper)
40
+ call_back.call(*arguments)
41
+ end
42
+
43
+ on(event_name, wrapper, priority)
44
+ end
45
+
46
+ # Emits an event.
47
+ #
48
+ # This method will return true if 0 or more listeners were succesfully
49
+ # handled. false is returned if one of the events broke the event chain.
50
+ #
51
+ # If the continueCallBack is specified, this callback will be called every
52
+ # time before the next event handler is called.
53
+ #
54
+ # If the continueCallback returns false, event propagation stops. This
55
+ # allows you to use the eventEmitter as a means for listeners to implement
56
+ # functionality in your application, and break the event loop as soon as
57
+ # some condition is fulfilled.
58
+ #
59
+ # Note that returning false from an event subscriber breaks propagation
60
+ # and returns false, but if the continue-callback stops propagation, this
61
+ # is still considered a 'successful' operation and returns true.
62
+ #
63
+ # Lastly, if there are 5 event handlers for an event. The continueCallback
64
+ # will be called at most 4 times.
65
+ #
66
+ # @param [String] event_name
67
+ # @param [Array] arguments
68
+ # @param [Proc, method] continue_call_back
69
+ # @return [Boolean]
70
+ def emit(event_name, arguments = [], continue_call_back = nil)
71
+ if !continue_call_back.is_a?(Proc)
72
+
73
+ listeners(event_name).each do |listener|
74
+ result = listener.call(*arguments)
75
+ return false if result == false
76
+ end
77
+ else
78
+ my_listeners = listeners(event_name)
79
+ counter = my_listeners.size
80
+
81
+ my_listeners.each do |listener|
82
+ counter -= 1
83
+ result = listener.call(*arguments)
84
+
85
+ return false if result == false
86
+
87
+ break if counter > 0 && !continue_call_back.call
88
+ end
89
+ end
90
+
91
+ true
92
+ end
93
+
94
+ # Returns the list of listeners for an event.
95
+ #
96
+ # The list is returned as an array, and the list of events are sorted by
97
+ # their priority.
98
+ #
99
+ # @param [String] event_name
100
+ # @return [Array<Proc, Method>]
101
+ def listeners(event_name)
102
+ return [] unless @listeners.key? event_name
103
+
104
+ # The list is not sorted
105
+ unless @listeners[event_name][0]
106
+ # Sorting
107
+ # array_multisort with ruby
108
+ joined = (0...@listeners[event_name][1].size).map do |i|
109
+ [@listeners[event_name][1][i], @listeners[event_name][2][i]]
110
+ end
111
+ sorted = joined.sort do |a, b|
112
+ a[0] <=> b[0]
113
+ end
114
+ sorted.each_with_index do |data, i|
115
+ @listeners[event_name][1][i] = data[0]
116
+ @listeners[event_name][2][i] = data[1]
117
+ end
118
+
119
+ # Marking the listeners as sorted
120
+ @listeners[event_name][0] = true
121
+ end
122
+
123
+ @listeners[event_name][2]
124
+ end
125
+
126
+ # Removes a specific listener from an event.
127
+ #
128
+ # If the listener could not be found, this method will return false. If it
129
+ # was removed it will return true.
130
+ #
131
+ # @param [String] event_name
132
+ # @param [Proc, Method] listener
133
+ # @return [Boolean]
134
+ def remove_listener(event_name, listener)
135
+ return false unless @listeners.key?(event_name)
136
+
137
+ @listeners[event_name][2].each_with_index do |check, index|
138
+ next unless check == listener
139
+
140
+ @listeners[event_name][1].delete_at(index)
141
+ @listeners[event_name][2].delete_at(index)
142
+ return true
143
+ end
144
+ false
145
+ end
146
+
147
+ # Removes all listeners.
148
+ #
149
+ # If the eventName argument is specified, all listeners for that event are
150
+ # removed. If it is not specified, every listener for every event is
151
+ # removed.
152
+ #
153
+ # @param [String] event_name
154
+ # @return [void]
155
+ def remove_all_listeners(event_name = nil)
156
+ if !event_name.nil?
157
+ @listeners.delete(event_name)
158
+ else
159
+ @listeners = {}
160
+ end
161
+ end
162
+
163
+ # TODO: document
164
+ def initialize_event_emitter_trait
165
+ @listeners = {}
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,196 @@
1
+ module Tilia
2
+ module Event
3
+ # An implementation of the Promise pattern.
4
+ #
5
+ # Promises basically allow you to avoid what is commonly called 'callback
6
+ # hell'. It allows for easily chaining of asynchronous operations.
7
+ class Promise
8
+ # Pending promise. No result yet.
9
+ PENDING = 0
10
+
11
+ # The promise has been fulfilled. It was successful.
12
+ FULFILLED = 1
13
+
14
+ # The promise was rejected. The operation failed.
15
+ REJECTED = 2
16
+
17
+ protected
18
+
19
+ # The current state of this promise.
20
+ #
21
+ # @return [Fixnum]
22
+ attr_accessor :state
23
+
24
+ # A list of subscribers. Subscribers are the callbacks that want us to let
25
+ # them know if the callback was fulfilled or rejected.
26
+ #
27
+ # @return [Array]
28
+ attr_accessor :subscribers
29
+
30
+ # The result of the promise.
31
+ #
32
+ # If the promise was fulfilled, this will be the result value. If the
33
+ # promise was rejected, this is most commonly an exception.
34
+ attr_accessor :value
35
+
36
+ public
37
+
38
+ # Creates the promise.
39
+ #
40
+ # The passed argument is the executor. The executor is automatically
41
+ # called with two arguments.
42
+ #
43
+ # Each are callbacks that map to self.fulfill and self.reject.
44
+ # Using the executor is optional.
45
+ #
46
+ # @param [Proc, Method] executor
47
+ # @return [void]
48
+ def initialize(executor = nil)
49
+ @state = PENDING
50
+ @subscribers = []
51
+ @value = nil
52
+
53
+ executor.call(method(:fulfill), method(:reject)) if executor
54
+ end
55
+
56
+ # This method allows you to specify the callback that will be called after
57
+ # the promise has been fulfilled or rejected.
58
+ #
59
+ # Both arguments are optional.
60
+ #
61
+ # This method returns a new promise, which can be used for chaining.
62
+ # If either the onFulfilled or onRejected callback is called, you may
63
+ # return a result from this callback.
64
+ #
65
+ # If the result of this callback is yet another promise, the result of
66
+ # _that_ promise will be used to set the result of the returned promise.
67
+ #
68
+ # If either of the callbacks return any other value, the returned promise
69
+ # is automatically fulfilled with that value.
70
+ #
71
+ # If either of the callbacks throw an exception, the returned promise will
72
+ # be rejected and the exception will be passed back.
73
+ #
74
+ # @param [Proc, Method] on_fulfilled
75
+ # @param [Proc, Method] on_rejected
76
+ # @return [Promise]
77
+ def then(on_fulfilled = nil, on_rejected = nil)
78
+ sub_promise = self.class.new
79
+ case @state
80
+ when PENDING
81
+ @subscribers << [sub_promise, on_fulfilled, on_rejected]
82
+ when FULFILLED
83
+ invoke_callback(sub_promise, on_fulfilled)
84
+ when REJECTED
85
+ invoke_callback(sub_promise, on_rejected)
86
+ end
87
+ sub_promise
88
+ end
89
+
90
+ # Add a callback for when this promise is rejected.
91
+ #
92
+ # I would have used the word 'catch', but it's a reserved word in PHP, so
93
+ # we're not allowed to call our function that.
94
+ #
95
+ # @param [Proc, Method] on_rejected
96
+ # @return [Promise]
97
+ def error(on_rejected)
98
+ self.then(nil, on_rejected)
99
+ end
100
+
101
+ # Marks this promise as fulfilled and sets its return value.
102
+ #
103
+ # @param value
104
+ # @return [void]
105
+ def fulfill(value = nil)
106
+ unless @state == PENDING
107
+ fail PromiseAlreadyResolvedException, 'This promise is already resolved, and you\'re not allowed to resolve a promise more than once'
108
+ end
109
+ @state = FULFILLED
110
+ @value = value
111
+ @subscribers.each do |subscriber|
112
+ invoke_callback(subscriber[0], subscriber[1])
113
+ end
114
+ end
115
+
116
+ # Marks this promise as rejected, and set it's rejection reason.
117
+ #
118
+ # @param reason
119
+ # @return [void]
120
+ def reject(reason = nil)
121
+ unless @state == PENDING
122
+ fail PromiseAlreadyResolvedException, 'This promise is already resolved, and you\'re not allowed to resolve a promise more than once'
123
+ end
124
+ @state = REJECTED
125
+ @value = reason
126
+ @subscribers.each do |subscriber|
127
+ invoke_callback(subscriber[0], subscriber[2])
128
+ end
129
+ end
130
+
131
+ # It's possible to send an array of promises to the all method. This
132
+ # method returns a promise that will be fulfilled, only if all the passed
133
+ # promises are fulfilled.
134
+ #
135
+ # @param [Array<Promise>] promises
136
+ # @return [Promise]
137
+ def self.all(promises)
138
+ new(
139
+ lambda do |success, failing|
140
+ success_count = 0
141
+ complete_result = []
142
+
143
+ promises.each_with_index do |sub_promise, promise_index|
144
+ sub_promise.then(
145
+ lambda do |result|
146
+ complete_result[promise_index] = result
147
+ success_count += 1
148
+
149
+ success.call(complete_result) if success_count == promises.size
150
+
151
+ return result
152
+ end
153
+ ).error(
154
+ lambda do |reason|
155
+ failing.call(reason)
156
+ end
157
+ )
158
+ end
159
+ end
160
+ )
161
+ end
162
+
163
+ protected
164
+
165
+ # This method is used to call either an onFulfilled or onRejected callback.
166
+ #
167
+ # This method makes sure that the result of these callbacks are handled
168
+ # correctly, and any chained promises are also correctly fulfilled or
169
+ # rejected.
170
+ #
171
+ # @param [Promise] sub_promise
172
+ # @param [Proc, Method] call_back
173
+ # @return [void]
174
+ def invoke_callback(sub_promise, call_back = nil)
175
+ if call_back.is_a?(Proc) || call_back.is_a?(Method)
176
+ begin
177
+ result = call_back.call(@value)
178
+ if result.is_a?(self.class)
179
+ result.then(sub_promise.method(:fulfill), sub_promise.method(:reject))
180
+ else
181
+ sub_promise.fulfill(result)
182
+ end
183
+ rescue => e
184
+ sub_promise.reject(e.to_s)
185
+ end
186
+ else
187
+ if @state == FULFILLED
188
+ sub_promise.fulfill(@value)
189
+ else
190
+ sub_promise.reject(@value)
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,8 @@
1
+ module Tilia
2
+ module Event
3
+ # This exception is thrown when the user tried to reject or fulfill a promise,
4
+ # after either of these actions were already performed.
5
+ class PromiseAlreadyResolvedException < RuntimeError
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ module Tilia
2
+ module Event
3
+ # This class contains the version number for this package.
4
+ class Version
5
+ # Full version number
6
+ VERSION = '2.0.2'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,89 @@
1
+ require 'test_helper'
2
+
3
+ module Tilia
4
+ module Event
5
+ class ContinueCallbackTest < Minitest::Test
6
+ def test_continue_call_back
7
+ ee = EventEmitter.new
8
+
9
+ handler_counter = 0
10
+ bla = lambda do
11
+ handler_counter += 1
12
+ end
13
+
14
+ ee.on('foo', bla)
15
+ ee.on('foo', bla)
16
+ ee.on('foo', bla)
17
+
18
+ continue_counter = 0
19
+ r = ee.emit(
20
+ 'foo',
21
+ [],
22
+ lambda do
23
+ continue_counter += 1
24
+ true
25
+ end
26
+ )
27
+
28
+ assert(r)
29
+ assert_equal(3, handler_counter)
30
+ assert_equal(2, continue_counter)
31
+ end
32
+
33
+ def test_continue_call_back_break
34
+ ee = EventEmitter.new
35
+
36
+ handler_counter = 0
37
+ bla = lambda do
38
+ handler_counter += 1
39
+ end
40
+
41
+ ee.on('foo', bla)
42
+ ee.on('foo', bla)
43
+ ee.on('foo', bla)
44
+
45
+ continue_counter = 0
46
+ r = ee.emit(
47
+ 'foo',
48
+ [],
49
+ lambda do
50
+ continue_counter += 1
51
+ false
52
+ end
53
+ )
54
+
55
+ assert(r)
56
+ assert_equal(1, handler_counter)
57
+ assert_equal(1, continue_counter)
58
+ end
59
+
60
+ def test_continue_call_back_break_by_handler
61
+ ee = EventEmitter.new
62
+
63
+ handler_counter = 0
64
+ bla = lambda do
65
+ handler_counter += 1
66
+ false
67
+ end
68
+
69
+ ee.on('foo', bla)
70
+ ee.on('foo', bla)
71
+ ee.on('foo', bla)
72
+
73
+ continue_counter = 0
74
+ r = ee.emit(
75
+ 'foo',
76
+ [],
77
+ lambda do
78
+ continue_counter += 1
79
+ false
80
+ end
81
+ )
82
+
83
+ refute(r)
84
+ assert_equal(1, handler_counter)
85
+ assert_equal(0, continue_counter)
86
+ end
87
+ end
88
+ end
89
+ end