elevate 0.6.0 → 0.7.0

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