elevate 0.6.0 → 0.7.0

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.
@@ -9,7 +9,7 @@ module HTTP
9
9
  @headers = nil
10
10
  @status_code = nil
11
11
  @error = nil
12
- @raw_body = nil
12
+ @raw_body = NSMutableData.alloc.init
13
13
  @url = nil
14
14
  end
15
15
 
@@ -17,7 +17,6 @@ module HTTP
17
17
  #
18
18
  # @api private
19
19
  def append_data(data)
20
- @raw_body ||= NSMutableData.alloc.init
21
20
  @raw_body.appendData(data)
22
21
  end
23
22
 
@@ -8,33 +8,12 @@ module Elevate
8
8
  # newly initialized instance
9
9
  #
10
10
  # @api private
11
- def initWithTarget(target, args:args)
11
+ def initWithTarget(target, args: args, channel: channel)
12
12
  if init
13
13
  @coordinator = IOCoordinator.new
14
- @context = TaskContext.new(args, &target)
15
- @timeout_callback = nil
16
- @timer = nil
17
- @update_callback = nil
18
- @finish_callback = nil
19
-
20
- setCompletionBlock(lambda do
21
- if @finish_callback
22
- @finish_callback.call(@result, @exception) unless isCancelled
23
- end
24
-
25
- Dispatch::Queue.main.sync do
26
- @context = nil
27
-
28
- if @timer
29
- @timer.invalidate
30
- @timer = nil
31
- end
32
-
33
- @timeout_callback = nil
34
- @update_callback = nil
35
- @finish_callback = nil
36
- end
37
- end)
14
+ @context = TaskContext.new(target, channel, args)
15
+ @exception = nil
16
+ @result = nil
38
17
  end
39
18
 
40
19
  self
@@ -51,56 +30,26 @@ module Elevate
51
30
  super
52
31
  end
53
32
 
54
- # Returns information about this task.
55
- #
56
- # @return [String]
57
- # String suitable for debugging purposes.
58
- #
59
- # @api public
60
- def inspect
61
- details = []
62
- details << "<canceled>" if @coordinator.cancelled?
63
-
64
- "#<#{self.class.name}: #{details.join(" ")}>"
65
- end
66
-
67
- # Logs debugging information in certain configurations.
68
- #
69
- # @return [void]
70
- #
71
- # @api private
72
- def log(line)
73
- puts line unless RUBYMOTION_ENV == "test"
74
- end
75
-
76
33
  # Runs the specified task.
77
34
  #
78
35
  # @return [void]
79
36
  #
80
37
  # @api private
81
38
  def main
82
- log " START: #{inspect}"
83
-
84
39
  @coordinator.install
85
40
 
86
41
  begin
87
42
  unless @coordinator.cancelled?
88
- @result = @context.execute do |*args|
89
- @update_callback.call(*args) if @update_callback
90
- end
43
+ @result = @context.execute
91
44
  end
92
45
 
93
- rescue Exception => e
46
+ rescue => e
94
47
  @exception = e
95
-
96
- if e.is_a?(TimeoutError)
97
- @timeout_callback.call if @timeout_callback
98
- end
99
48
  end
100
49
 
101
50
  @coordinator.uninstall
102
51
 
103
- log "FINISH: #{inspect}"
52
+ @context = nil
104
53
  end
105
54
 
106
55
  # Returns the exception that terminated this task, if any.
@@ -123,96 +72,14 @@ module Elevate
123
72
  # @api public
124
73
  attr_reader :result
125
74
 
126
- # Sets the callback to be run upon completion of this task. Do not call
127
- # this method after the task has started.
128
- #
129
- # @param callback [Elevate::Callback]
130
- # completion callback
131
- #
132
- # @return [void]
133
- #
134
- # @api private
135
- def on_finish=(callback)
136
- @finish_callback = callback
137
- end
138
-
139
- # Sets the callback to be run when this task is queued.
140
- #
141
- # Do not call this method after the task has started.
142
- #
143
- # @param callback [Elevate::Callback]
144
- # callback to be invoked when queueing
145
- #
146
- # @return [void]
147
- #
148
- # @api private
149
- def on_start=(callback)
150
- start_callback = callback
151
- start_callback.retain
152
-
153
- Dispatch::Queue.main.async do
154
- start_callback.call unless isCancelled
155
- start_callback.release
156
- end
157
- end
158
-
159
- # Handles timeout expiration.
160
- #
161
- # @return [void]
162
- #
163
- # @api private
164
- def on_timeout_elapsed(timer)
165
- @coordinator.cancel(TimeoutError)
166
- end
167
-
168
- # Sets the timeout callback.
169
- #
170
- # @param callback [Elevate::Callback]
171
- # callback to run on timeout
172
- #
173
- # @return [void]
174
- #
175
- # @api private
176
- def on_timeout=(callback)
177
- @timeout_callback = callback
178
- end
179
-
180
- # Sets the update callback, which is invoked for any yield statements in the task.
181
- #
182
- # @param callback [Elevate::Callback]
183
- # @return [void]
184
- #
185
- # @api private
186
- def on_update=(callback)
187
- @update_callback = callback
188
- end
189
-
190
- # Sets the timeout interval for this task.
191
- #
192
- # The timeout starts when the task is queued, not when it is started.
193
- #
194
- # @param interval [Fixnum]
195
- # seconds to allow for task completion
75
+ # Cancels any waiting operation with a TimeoutError, interrupting
76
+ # execution. This is not the same as #cancel.
196
77
  #
197
78
  # @return [void]
198
79
  #
199
- # @api private
200
- def timeout=(interval)
201
- @timer = NSTimer.scheduledTimerWithTimeInterval(interval,
202
- target: self,
203
- selector: :"on_timeout_elapsed:",
204
- userInfo: nil,
205
- repeats: false)
206
- end
207
-
208
- # Returns whether this task timed out.
209
- #
210
- # @return [Boolean]
211
- # true if this task was aborted due to a time out.
212
- #
213
80
  # @api public
214
- def timed_out?
215
- @exception.class == TimeoutError
81
+ def timeout
82
+ @coordinator.cancel(TimeoutError)
216
83
  end
217
84
  end
218
85
  end
@@ -0,0 +1,122 @@
1
+ module Elevate
2
+ class Task
3
+ def initialize(definition, controller, active_tasks)
4
+ @definition = definition
5
+ @controller = WeakRef.new(controller)
6
+ @active_tasks = active_tasks
7
+ @operation = nil
8
+ @channel = Channel.new(method(:on_update))
9
+ @args = nil
10
+ @timer = nil
11
+ end
12
+
13
+ def cancel
14
+ if @operation
15
+ @operation.cancel
16
+
17
+ if @timer
18
+ @timer.invalidate
19
+ end
20
+ end
21
+ end
22
+
23
+ def name
24
+ @definition.name
25
+ end
26
+
27
+ def start(args)
28
+ @operation = ElevateOperation.alloc.initWithTarget(@definition.handlers[:background],
29
+ args: args,
30
+ channel: WeakRef.new(@channel))
31
+
32
+ @operation.addObserver(self, forKeyPath: "isFinished", options: NSKeyValueObservingOptionNew, context: nil)
33
+ queue.addOperation(@operation)
34
+ @active_tasks << self
35
+
36
+ @args = args
37
+
38
+ if interval = @definition.options[:timeout_interval]
39
+ @timer = NSTimer.scheduledTimerWithTimeInterval(interval,
40
+ target: self,
41
+ selector: :"on_timeout:",
42
+ userInfo: nil,
43
+ repeats: false)
44
+ end
45
+
46
+ performSelectorOnMainThread(:on_start, withObject: nil, waitUntilDone: false)
47
+ end
48
+
49
+ private
50
+
51
+ def error_handler_for(exception)
52
+ handler_name = exception.class.name
53
+ handler_name = handler_name.split("::").last
54
+ handler_name.gsub!(/Error$/, "")
55
+ handler_name.gsub!(/(.)([A-Z])/) { |m| "#{$1}_#{$2.downcase}" }
56
+ handler_name = "on_" + handler_name.downcase
57
+
58
+ handler_name.to_sym
59
+ end
60
+
61
+ def invoke(handler_name, *args)
62
+ return false if @operation.isCancelled
63
+
64
+ block = @definition.handlers[handler_name]
65
+ return false unless block
66
+
67
+ @controller.task_args = @args
68
+ @controller.instance_exec(*args, &block)
69
+ @controller.task_args = nil
70
+
71
+ true
72
+ end
73
+
74
+ def queue
75
+ Dispatch.once do
76
+ $elevate_queue = NSOperationQueue.alloc.init
77
+ $elevate_queue.maxConcurrentOperationCount = 1
78
+ end
79
+
80
+ $elevate_queue
81
+ end
82
+
83
+ def observeValueForKeyPath(path, ofObject: operation, change: change, context: ctx)
84
+ case path
85
+ when "isFinished"
86
+ performSelectorOnMainThread(:on_finish, withObject: nil, waitUntilDone: false)
87
+ end
88
+ end
89
+
90
+ def on_start
91
+ invoke(:on_start)
92
+ end
93
+
94
+ def on_finish
95
+ @operation.removeObserver(self, forKeyPath: "isFinished")
96
+ @active_tasks.delete(self)
97
+
98
+ if @timer
99
+ @timer.invalidate
100
+ end
101
+
102
+ if exception = @operation.exception
103
+ invoke(error_handler_for(exception), exception) || invoke(:on_error, exception)
104
+ end
105
+
106
+ invoke(:on_finish, @operation.result, @operation.exception)
107
+ end
108
+
109
+ def on_timeout(timer)
110
+ @operation.timeout
111
+ end
112
+
113
+ def on_update(args)
114
+ unless NSThread.isMainThread
115
+ performSelectorOnMainThread(:"on_update:", withObject: args, waitUntilDone: false)
116
+ return
117
+ end
118
+
119
+ invoke(:on_update, *args)
120
+ end
121
+ end
122
+ end
@@ -2,17 +2,26 @@ module Elevate
2
2
  # A blank slate for hosting task blocks.
3
3
  #
4
4
  # Because task blocks run in another thread, it is dangerous to expose them
5
- # to the calling context. This class acts as a sandbox for task blocks.
5
+ # to the calling context. This class acts as a sandbox for task blocks.
6
6
  #
7
7
  # @api private
8
8
  class TaskContext
9
- def initialize(args, &block)
10
- metaclass = class << self; self; end
11
- metaclass.send(:define_method, :execute, &block)
9
+ def initialize(block, channel, args)
10
+ @__block = block
11
+ @__channel = channel
12
+ @__args = args
13
+ end
14
+
15
+ def execute
16
+ instance_exec(&@__block)
17
+ end
18
+
19
+ def task_args
20
+ @__args
21
+ end
12
22
 
13
- args.each do |key, value|
14
- instance_variable_set("@#{key}", value)
15
- end
23
+ def update(*args)
24
+ @__channel << args if @__channel
16
25
  end
17
26
  end
18
27
  end
@@ -0,0 +1,62 @@
1
+ module Elevate
2
+ class TaskDefinition
3
+ def initialize(name, options, &block)
4
+ @name = name
5
+ @options = options
6
+ @handlers = {}
7
+
8
+ instance_eval(&block)
9
+ end
10
+
11
+ attr_reader :name
12
+ attr_reader :handlers
13
+ attr_reader :options
14
+
15
+ def method_missing(method, *args, &block)
16
+ if method.to_s.start_with?("on_")
17
+ raise ArgumentError, "wrong number of arguments" unless args.empty?
18
+ raise ArgumentError, "block not supplied" unless block_given?
19
+
20
+ @handlers[method.to_sym] = block
21
+ else
22
+ super
23
+ end
24
+ end
25
+
26
+ def respond_to_missing?(method, include_private = false)
27
+ method.to_s.start_with?("on_") || super
28
+ end
29
+
30
+ def background(&block)
31
+ @handlers[:background] = block
32
+ end
33
+
34
+ def on_error(&block)
35
+ raise "on_error blocks must accept one parameter" unless block.arity == 1
36
+
37
+ @handlers[:on_error] = block
38
+ end
39
+
40
+ def on_finish(&block)
41
+ raise "on_finish blocks must accept two parameters" unless block.arity == 2
42
+
43
+ @handlers[:on_finish] = block
44
+ end
45
+
46
+ def on_start(&block)
47
+ raise "on_start blocks must accept zero parameters" unless block.arity == 0
48
+
49
+ @handlers[:on_start] = block
50
+ end
51
+
52
+ def on_update(&block)
53
+ @handlers[:on_update] = block
54
+ end
55
+
56
+ def timeout(seconds)
57
+ raise "timeout argument must be a number" unless seconds.is_a?(Numeric)
58
+
59
+ @options[:timeout_interval] = seconds
60
+ end
61
+ end
62
+ end
@@ -1,3 +1,3 @@
1
1
  module Elevate
2
- VERSION = "0.6.0"
2
+ VERSION = "0.7.0"
3
3
  end
@@ -1,159 +1,311 @@
1
- module Bacon
2
- class Context
3
- include ::Elevate
1
+ class TestController
2
+ include Elevate
3
+
4
+ def initialize
5
+ @invocations = {}
6
+ @counter = 0
7
+ @threads = []
8
+ @updates = []
9
+ @callback_args = nil
10
+ @completion = nil
11
+ end
12
+
13
+ attr_accessor :started
14
+ attr_accessor :result
15
+ attr_accessor :exception
16
+ attr_accessor :invocations
17
+ attr_accessor :counter
18
+ attr_accessor :threads
19
+ attr_accessor :updates
20
+ attr_accessor :callback_args
21
+ attr_accessor :completion
22
+
23
+ task :cancellable do
24
+ background do
25
+ task_args[:semaphore].wait
26
+ update 42
27
+
28
+ nil
29
+ end
30
+
31
+ on_start do
32
+ self.invocations[:start] = counter
33
+ self.counter += 1
34
+ end
35
+
36
+ on_finish do |result, ex|
37
+ self.invocations[:finish] = counter
38
+ self.counter += 1
39
+ end
40
+
41
+ on_update do |n|
42
+ self.invocations[:update] = counter
43
+ self.counter += 1
44
+ end
45
+ end
46
+
47
+ task :custom_error_handlers do
48
+ background do
49
+ raise TimeoutError
50
+ end
51
+
52
+ on_error do |e|
53
+ self.invocations[:error] = counter
54
+ self.counter += 1
55
+ end
56
+
57
+ on_timeout do |e|
58
+ self.invocations[:timeout] = counter
59
+ self.counter += 1
60
+ end
61
+ end
62
+
63
+ task :test_task do
64
+ background do
65
+ sleep 0.05
66
+ update 1
67
+ raise Elevate::TimeoutError if task_args[:raise]
68
+ sleep 0.1
69
+ update 2
70
+
71
+ 42
72
+ end
73
+
74
+ on_start do
75
+ self.invocations[:start] = counter
76
+ self.callback_args = task_args
77
+ self.counter += 1
78
+
79
+ self.threads << NSThread.currentThread
80
+ end
81
+
82
+ on_update do |num|
83
+ self.updates << num
84
+
85
+ self.invocations[:update] = counter
86
+ self.counter += 1
87
+
88
+ self.threads << NSThread.currentThread
89
+ end
90
+
91
+ on_error do |e|
92
+ self.invocations[:error] = counter
93
+ self.counter += 1
94
+ end
95
+
96
+ on_finish do |result, exception|
97
+ self.invocations[:finish] = counter
98
+ self.counter += 1
99
+
100
+ self.threads << NSThread.currentThread
101
+
102
+ self.result = result
103
+ self.exception = exception
104
+
105
+ #Dispatch::Queue.main.async { resume }
106
+ end
107
+ end
108
+
109
+ task :timeout_test do
110
+ timeout 0.3
111
+
112
+ background do
113
+ Elevate::HTTP.get("http://example.com/")
114
+ end
115
+
116
+ on_timeout do |e|
117
+ self.invocations[:timeout] = counter
118
+ self.counter += 1
119
+ end
120
+
121
+ on_finish do |result, ex|
122
+ self.invocations[:finish] = counter
123
+ self.counter += 1
124
+
125
+ @completion.call if @completion
126
+ end
4
127
  end
5
128
  end
6
129
 
7
130
  describe Elevate do
8
131
  extend WebStub::SpecHelpers
9
132
 
10
- describe "#async" do
11
- it "runs the specified task asynchronously" do
12
- async do
13
- task do
14
- true
15
- end
133
+ before do
134
+ @controller = TestController.new
135
+ end
16
136
 
17
- on_finish do |result, exception|
18
- @called = result
19
- resume
20
- end
137
+ describe "#cancel" do
138
+ describe "when no tasks are running" do
139
+ it "does nothing" do
140
+ ->{ @controller.cancel(:test_task) }.should.not.raise
21
141
  end
142
+ end
22
143
 
23
- wait_max 1.0 do
24
- @called.should.be.true
144
+ describe "when a single task is running" do
145
+ it "cancels the task and does not invoke callbacks" do
146
+ semaphore = Dispatch::Semaphore.new(0)
147
+ @controller.launch(:cancellable, semaphore: semaphore)
148
+
149
+ @controller.cancel(:cancellable)
150
+ semaphore.signal
151
+
152
+ wait 0.5 do
153
+ @controller.invocations[:update].should.be.nil
154
+ @controller.invocations[:finish].should.be.nil
155
+ end
25
156
  end
26
157
  end
27
158
 
28
- it "passes provided args to the task as instance variables" do
29
- async name: "harry" do
30
- task do
31
- @name
32
- end
159
+ describe "when several tasks are running" do
160
+ it "cancels all of them" do
161
+ semaphore = Dispatch::Semaphore.new(0)
162
+
163
+ @controller.launch(:cancellable, semaphore: semaphore)
164
+ @controller.launch(:cancellable, semaphore: semaphore)
165
+ @controller.launch(:cancellable, semaphore: semaphore)
166
+
167
+ @controller.cancel(:cancellable)
168
+ semaphore.signal
33
169
 
34
- on_finish do |name, exception|
35
- @result = name
36
- resume
170
+ wait 0.5 do
171
+ @controller.invocations[:update].should.be.nil
172
+ @controller.invocations[:finish].should.be.nil
37
173
  end
174
+
38
175
  end
176
+ end
177
+ end
39
178
 
40
- wait_max 1.0 do
41
- @result.should == "harry"
179
+ describe "#launch" do
180
+ it "runs the task asynchronously, returning the result" do
181
+ @controller.launch(:test_task, raise: false)
182
+
183
+ wait 0.5 do
184
+ @controller.result.should == 42
42
185
  end
43
186
  end
44
187
 
45
188
  it "allows tasks to report progress" do
46
- @updates = []
47
-
48
- async do
49
- task do
50
- sleep 0.1
51
- yield 1
52
- sleep 0.2
53
- yield 2
54
- sleep 0.3
55
- yield 3
56
-
57
- true
58
- end
189
+ @controller.launch(:test_task, raise: false)
59
190
 
60
- on_update do |count|
61
- @updates << count
62
- end
63
-
64
- on_finish do |result, exception|
65
- resume
66
- end
191
+ wait 0.5 do
192
+ @controller.updates.should == [1, 2]
67
193
  end
194
+ end
68
195
 
69
- wait_max 1.0 do
70
- @updates.should == [1,2,3]
196
+ it "invokes all callbacks on the UI thread" do
197
+ @controller.launch(:test_task, raise: false)
198
+
199
+ wait 0.5 do
200
+ @controller.threads.each { |t| t.isMainThread.should.be.true }
71
201
  end
72
202
  end
73
203
 
74
- describe "timeouts" do
75
- before do
76
- stub_request(:get, "http://example.com/").
77
- to_return(body: "Hello!", content_type: "text/plain", delay: 1.0)
204
+ it "sets task_args to args used at launch" do
205
+ @controller.launch(:test_task, raise: true)
206
+
207
+ wait 0.5 do
208
+ @controller.callback_args.should == { raise: true }
78
209
  end
210
+ end
79
211
 
80
- it "does not cancel the operation if it completes in time" do
81
- @timed_out = false
212
+ it "invokes on_error when an exception occurs" do
213
+ @controller.launch(:test_task, raise: true)
82
214
 
83
- async do
84
- timeout 3.0
215
+ wait 0.5 do
216
+ @controller.invocations[:error].should.not.be.nil
217
+ end
218
+ end
85
219
 
86
- task do
87
- Elevate::HTTP.get("http://example.com/")
220
+ it "invokes on_start before on_finish" do
221
+ @controller.launch(:test_task, raise: false)
88
222
 
89
- "finished"
90
- end
223
+ wait 0.5 do
224
+ @controller.invocations[:start].should < @controller.invocations[:finish]
225
+ end
226
+ end
91
227
 
92
- on_finish do |result, exception|
93
- @result = result
94
- resume
95
- end
96
- end
228
+ it "invokes on_update before on_finish" do
229
+ @controller.launch(:test_task, raise: false)
97
230
 
98
- wait_max 5.0 do
99
- @result.should == "finished"
100
- @timed_out.should.be.false
101
- end
231
+ wait 0.5 do
232
+ invocations = @controller.invocations
233
+
234
+ invocations[:update].should < invocations[:finish]
102
235
  end
236
+ end
103
237
 
104
- it "stops the operation when timeout interval has elapsed" do
105
- @result = nil
238
+ it "invokes on_update after on_start" do
239
+ @controller.launch(:test_task, raise: false)
240
+
241
+ wait 0.5 do
242
+ invocations = @controller.invocations
243
+
244
+ invocations[:update].should > invocations[:start]
245
+ end
246
+ end
247
+ end
106
248
 
107
- @task = async do
108
- timeout 0.5
249
+ describe "error handling" do
250
+ it "invokes an error handling correspding to the raised exception" do
251
+ @controller.launch(:custom_error_handlers)
109
252
 
110
- task do
111
- Elevate::HTTP.get("http://example.com/")
253
+ wait 0.5 do
254
+ invocations = @controller.invocations
112
255
 
113
- "finished"
114
- end
256
+ invocations[:timeout].should.not.be.nil
257
+ invocations[:error].should.be.nil
258
+ end
259
+ end
115
260
 
116
- on_finish do |result, exception|
117
- @result = result
118
- resume
119
- end
120
- end
261
+ it "invokes on_error if there is not a specific handler" do
262
+ @controller.launch(:test_task, raise: true)
121
263
 
122
- wait_max 5.0 do
123
- @result.should.not == "finished"
264
+ wait 0.5 do
265
+ invocations = @controller.invocations
124
266
 
125
- @task.timed_out?.should.be.true
126
- end
267
+ invocations[:error].should.not.be.nil
127
268
  end
269
+ end
270
+ end
128
271
 
129
- it "invokes on_timeout when a timeout occurs" do
130
- @result = ""
131
- @timed_out = false
272
+ describe "timeouts" do
273
+ it "does not cancel the operation if it completes in time" do
274
+ stub_request(:get, "http://example.com/").
275
+ to_return(body: "Hello!", content_type: "text/plain")
132
276
 
133
- async do
134
- timeout 0.5
277
+ @controller.completion = -> { resume }
278
+ @controller.launch(:timeout_test)
135
279
 
136
- task do
137
- Elevate::HTTP.get("http://example.com/")
280
+ wait_max 0.9 do
281
+ @controller.invocations[:timeout].should.be.nil
282
+ @controller.invocations[:finish].should.not.be.nil
283
+ end
284
+ end
138
285
 
139
- "finished"
140
- end
286
+ it "stops the operation when it exceeds the timeout" do
287
+ stub_request(:get, "http://example.com/").
288
+ to_return(body: "Hello!", content_type: "text/plain", delay: 1.0)
141
289
 
142
- on_timeout do
143
- @timed_out = true
144
- end
290
+ @controller.completion = -> { resume }
291
+ @controller.launch(:timeout_test)
145
292
 
146
- on_finish do |result, exception|
147
- @result = result
148
- resume
149
- end
150
- end
293
+ wait_max 0.9 do
294
+ @controller.invocations[:finish].should.not.be.nil
295
+ end
296
+ end
151
297
 
152
- wait_max 5.0 do
153
- @result.should.not == "finished"
154
- @timed_out.should.be.true
155
- end
298
+ it "invokes on_timeout when the operation times out" do
299
+ stub_request(:get, "http://example.com/").
300
+ to_return(body: "Hello!", content_type: "text/plain", delay: 1.0)
301
+
302
+ @controller.completion = -> { resume }
303
+ @controller.launch(:timeout_test)
304
+
305
+ wait_max 0.9 do
306
+ @controller.invocations[:timeout].should.not.be.nil
156
307
  end
308
+
157
309
  end
158
310
  end
159
311
  end