tilia-event 2.0.2

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.
@@ -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