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.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rubocop.yml +32 -0
- data/.simplecov +4 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.sabre.md +51 -0
- data/CONTRIBUTING.md +25 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +51 -0
- data/LICENSE +27 -0
- data/LICENSE.sabre +27 -0
- data/README.md +37 -0
- data/Rakefile +18 -0
- data/lib/tilia/event.rb +16 -0
- data/lib/tilia/event/event_emitter.rb +16 -0
- data/lib/tilia/event/event_emitter_interface.rb +86 -0
- data/lib/tilia/event/event_emitter_trait.rb +169 -0
- data/lib/tilia/event/promise.rb +196 -0
- data/lib/tilia/event/promise_already_resolved_exception.rb +8 -0
- data/lib/tilia/event/version.rb +9 -0
- data/test/continue_callback_test.rb +89 -0
- data/test/event_emitter_test.rb +220 -0
- data/test/promise_test.rb +164 -0
- data/test/test_helper.rb +4 -0
- data/tilia-event.gemspec +13 -0
- metadata +82 -0
@@ -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,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
|